三数之和的解决之路= =
题干表述
测试案列(部分)
第一次思路
这种其实是最暴力的,也是我脑海里第一个想到的最简单的方法了。
思路就是三个循环,一个循环去一个数,然后当三个下标不同,且对应的三个数相加为0时就append
追加进列表
# -*- encoding: utf-8 -*-
class Solution(object):
def threeSum(self, nums):
"""
:type nums: List[int]
:rtype: List[List[int]]
"""
res_list = []
index = range(len(nums))
for i in index:
for j in index:
for k in index:
if i!=j and j!=k and i!=k and nums[i]+nums[j]+nums[k]==0:
res_list.append([nums[i], nums[j], nums[k]])
return res_list
这种肯定会有错的,运行之后会发现,重复了很多排列顺序不同的三个同一元素。所以我们暂不考虑减少时间复杂度的情况,先考虑怎么解决去重的问题。
第二次
我们对于第一次代码中的三个!=
进行优化,其实用小于号更好,既能避免下标重复,又能避免去重问题。
# -*- encoding: utf-8 -*-
class Solution(object):
def threeSum(self, nums):
"""
:type nums: List[int]
:rtype: List[List[int]]
"""
res_list = []
index = range(len(nums))
for i in index:
for j in index:
for k in index:
if i<j and j<k and nums[i]+nums[j]+nums[k]==0:
res_list.append([nums[i], nums[j], nums[k]])
return res_list
但结果还是不对,小于号虽然能解决一点去重问题,但是也就只能解决一点点。。
顺序不同的三个同一元素序列问题解决了,但是存在三个元素下标不同,但三个元素与另外三个元素重复的情况。
比如上图,两个[-1, 0, 1]
其实是下标为0的-1
和下标为4的-1
造成的重复。
第三次
那if
判断的条件就得更严格,res_list里面有的列表,就不往里面添加。
# -*- encoding: utf-8 -*-
class Solution(object):
def threeSum(self, nums):
"""
:type nums: List[int]
:rtype: List[List[int]]
"""
res_list = []
index = range(len(nums))
for i in index:
for j in index:
for k in index:
if i<j and j<k and nums[i]+nums[j]+nums[k]==0:
if ([nums[i], nums[j], nums[k]] not in res_list) and ([nums[j], nums[i], nums[k]] not in res_list) and ([nums[j], nums[k], nums[i]] not in res_list) and ([nums[i], nums[k], nums[j]] not in res_list) and ([nums[k], nums[j], nums[i]] not in res_list) and ([nums[k], nums[i], nums[j]] not in res_list):
res_list.append([nums[i], nums[j], nums[k]])
return res_list
这个测试是正确的,但是也是暴力解法,看起来太蠢了,哈哈哈,这么长的枚举判断我自己都受不了
但是提交会发现,超出时间限制。
执行到超时的案例是数值比较极端一个数据。
第四次
那肯定是那个枚举的if
判断需要改善,因为想不到还有什么办法,翻了翻评论区,发现有人的代码第一步是将输入序列排序,自己立马想到,可能有序序列就不需要这种枚举,可能只需要一个[nums[i], nums[j], nums[k]] not in res_list
即可
# -*- encoding: utf-8 -*-
class Solution(object):
def threeSum(self, nums):
"""
:type nums: List[int]
:rtype: List[List[int]]
"""
nums.sort()
res_list = []
index = range(len(nums))
for i in index:
for j in index:
for k in index:
if i<j and j<k and nums[i]+nums[j]+nums[k]==0:
if [nums[i], nums[j], nums[k]] not in res_list:
res_list.append([nums[i], nums[j], nums[k]])
return res_list
排序之后,可以不用那么多if条件了,测试没问题,但提交还是超时了额
第五次
我想可能是三次循环太多了,这个if
倒是可以往后放一放。
翻了翻评论区,注意到了一个哈希法
但后半部分提到的vector我只知道是向量,之前自己学Spark和数据结构的时候见过,但也只是知道它是向量而已,多的就不知道了。
但是前半部分的思路可以借鉴,通过0-a-b
算出c
,可以少一个循环。
# -*- encoding: utf-8 -*-
class Solution(object):
def threeSum(self, nums):
"""
:type nums: List[int]
:rtype: List[List[int]]
"""
nums.sort()
res_list = []
n = len(nums)
for i in range(n):
for j in range(i+1, n):
res = 0-nums[i]-nums[j]
if res in nums[j+1:]:
if [nums[i], nums[j], res] not in res_list:
res_list.append([nums[i], nums[j], res])
return res_list
改良了一下,改成了双循环,但是还是超时了??可能是range()比较耗时?不理解。。
第六次
参考了评论区的双指针法
有点像快排的每趟思路,首先要确立好基本思路,就是先排序为有序序列,然后每趟for循环从前到后选一个基准,然后对这个基准后面的序列进行两个元素的选取,使三个元素相加=0
。
然后这个选取两个元素的过程是,先取两头left
(i+1的位置)和right
(n-1的位置),然后sum
大了,right-1
;sum
小了left+1
(因为是增序序列,所以这种逻辑是可以的,这里可能不常了解算法的人会烧CPU,可以反复理解一下,最好拿张纸写个序列试一试)。
# -*- encoding: utf-8 -*-
class Solution(object):
def threeSum(self, nums):
"""
:type nums: List[int]
:rtype: List[List[int]]
"""
nums.sort()
res_list = []
n = len(nums)
for i in range(n):
left = i+1
right = n-1
while True:
if left >= right:
break
if nums[i]+nums[left]+nums[right] > 0:
right -= 1
continue
elif nums[i]+nums[left]+nums[right] < 0:
left += 1
continue
else:
if [nums[i], nums[left], nums[right]] not in res_list:
res_list.append([nums[i], nums[left], nums[right]])
break
return res_list
按双指针思路写的代码,可惜,最后还是超时了。但最后的输入数据变了,是[0,0,0,0]
没过。
第七次
对比了一下别人写的代码,修改了第七版的代码,发现了第六版还有些许错误,都在下面代码的注释里。
# -*- encoding: utf-8 -*-
class Solution(object):
def threeSum(self, nums):
"""
:type nums: List[int]
:rtype: List[List[int]]
"""
nums.sort()
res_list = []
n = len(nums)
for i in range(n):
left = i+1
right = n-1
# 增序序列第一个元素大于0,三数之和就不可能为0
if nums[i] > 0:
break
# 没搞懂,暂先不加试试
# if i >= 1 and nums[i] == nums[i-1]:
# continue
while left < right: # left<right这一限制条件就可以直接写到这来
s = nums[i]+nums[left]+nums[right] # 求和结果用到了两次,就可以先存储一个变量
if s > 0:
right -= 1
# continue 加了等于没加,不需要continue
elif s < 0:
left += 1
# continue 加了等于没加,不需要continue
else:
if [nums[i], nums[left], nums[right]] not in res_list:
res_list.append([nums[i], nums[left], nums[right]])
# 这里不应该break,而是继续找下去,不然会漏
# break
left += 1
right -= 1
return res_list
当然,代码里面还是有我的坚持。。。。原代码有两处没理解什么意思所以没加,先试了试我这一拼凑改良版。
这次连第一个测试案例都过不了,应该是死循环了。
第八次(抄的)
class Solution:
def threeSum(self, nums):
ans = []
n = len(nums)
nums.sort()
for i in range(n):
left = i + 1
right = n - 1
if nums[i] > 0:
break
if i >= 1 and nums[i] == nums[i - 1]:
continue
while left < right:
total = nums[i] + nums[left] + nums[right]
if total > 0:
right -= 1
elif total < 0:
left += 1
else:
ans.append([nums[i], nums[left], nums[right]])
while left != right and nums[left] == nums[left + 1]: left += 1
while left != right and nums[right] == nums[right - 1]: right -= 1
left += 1
right -= 1
return ans
和自己一开始到最后的思路不同的是以下几个点:
- 能减少循环次数的就要减少。
比如循环一开始的这个语句
if nums[i] > 0:
break
升序序列第一个元素就比0大,那三个数的和就肯定>=0
了,就不需要进行后续的while
循环了。
- 去重的方法太费时间了,nums很长的时候
if [nums[i], nums[left], nums[right]] not in res_list
也得要O(n),这个点最关键,体现的地方有两处代码。
# 第一处(循环一开始处的语句)
if i >= 1 and nums[i] == nums[i - 1]:
continue
# 第二处(while循环中else分支的语句)
while left != right and nums[left] == nums[left + 1]: left += 1
while left != right and nums[right] == nums[right - 1]: right -= 1
第一处是如何巧妙去重的呢,自己单看第一遍的时候没有理解,在手动模拟了一遍流程渐渐理解了。
整体流程(只手写了三轮循环):
过程描述:
序列[-1, 0, 1, 2, -1, -4]
进入函数,首先会被升序排序变为[-4, -1, -1, 0, 1, 2]
,然后开始循环。
【第一轮for循环】i
指向下标为0
的地方(也就是-4
),按规矩left
就指向下标为0+1=1
的地方(也就是-1
),right
指向下标为6-1=5
的地方(也就是2
)
此时total=-4+(-1)+2=-3<0
,所以进行left+1
操作。
然后可以看出,经过四次left+1
操作,因left=right
,导致while循环结束,第一轮for循环结束。
此轮循环中,left指针移动4次、right指针移动0次、比较次数应为4次。
【第二轮for循环】i
指向下标为1
的地方(也就是-1
),按规矩left
就指向下标为1+1=2
的地方(也就是-1
),right
指向下标为6-1=5
的地方(也就是2
)
此时total=-1+(-1)+2=0
,所以进行append()
操作(添加了[-1, -1, 2]
),向结果列表res中追加此时的三元组。但是,while循环并没有结束!(之前自己算法设计的思路就break结束了,会导致漏掉的情况)
此时同时进行left+1
,right-1
操作,然后继续寻找是否存在更多的合法三元组。然后又找到了[-1, 0, 1]
,继续append()
,然后继续对left
和right
进行操作,导致while循环条件不成立,while循环结束,for循环结束。
此轮循环中,left指针移动2次,right指针移动了2次,比较次数应为2次。
【第三轮for循环】i
指向下标为2
的地方(也就是-1
),此时判断nums[i] == nums[i - 1]
,就会跳过while循环,直接开始第四轮for循环。
但是,为什么这样就跳过了呢?这样就能避免重复?先拿这个栗子说明。
下标为1的-1可以在下标从2到5的4个元素中有两组数据符合要求,那么下标为2的-1在下标从3到5的3个元素中,只有两个可能,一个是有符合要求的数据,一个是没有。有符合要求的数据,那么==一定是重复的==,可以跳过!没有,那也就不用往结果列表里面追加,所以可以跳过。
所以,nums[i] == nums[i - 1]
的条件下,就是可以直接跳过,不用费时间。
至于第二处的代码,也是同理。只能说巧妙。。。知识upup