题目描述
Given an integer array nums, return all the triplets [nums[i], nums[j], nums[k]] such that i ! = j i != j i!=j, i ! = k i != k i!=k, and j ! = k j != k j!=k, and n u m s [ i ] + n u m s [ j ] + n u m s [ k ] = = 0 nums[i] + nums[j] + nums[k] == 0 nums[i]+nums[j]+nums[k]==0.
Notice that the solution set must not contain duplicate triplets.
Example 1:
Input: nums = [-1,0,1,2,-1,-4]
Output: [[-1,-1,2],[-1,0,1]]
Example 2:
Input: nums = []
Output: []
Constraints:
0 < = n u m s . l e n g t h < = 3000 0 <= nums.length <= 3000 0<=nums.length<=3000
− 1 0 5 < = n u m s [ i ] < = 1 0 5 -10^5 <= nums[i] <= 10^5 −105<=nums[i]<=105
解题思路
直接基于三层循环的思路
无论是算法新手还是老司机,这个方法应该都是极易想到并且理解的。设定三层循环,找到其所有的元素个数为3的子集并完成子集合并即可实现,找到其中和为0的子集即为最后结果。
def threeSum_General(self, nums: List[int]) -> List[List[int]]:
# 元素数目小于3个,不存在解
if len(nums) < 3:
return []
# 元素个数刚好为3个,如果恰好为0,则为
if len(nums) == 3:
if sum(nums) == 0:
return [nums]
else:
return []
# 元素个数>3个
result = []
# 3层循环
for i in range(0, len(nums)):
for j in range(i + 1, len(nums)):
for k in range(j + 1, len(nums)):
# 判断是否和为零
if nums[i] + nums[j] + nums[k] == 0:
# 满足条件的话对三个元素进行排序,并检查在结果集合中是否已经存在该结果
temp = sorted([nums[i], nums[j], nums[k]])
# 如果还不存在该解,将其加入最终的解集
if temp not in result:
result.append(list(temp))
return result
但是在使用上述方法时,leetcode直接就报了超时的错误。不难理解,即便数组长度范围限制在3000以内,这种 O ( N 3 ) O(N^3) O(N3)的解法的耗时也是极其可观的。为此,只能另寻别路。
先排序,再循环的解决方法
之所以考虑先排序,是因为在上述的做法中我们每一步都要做一次排序的工作,因此不如直接对整个数组做排序,随后采用三层循环的方案。这样子可以直接保证三个数字的顺序,代码如下:
def threeSum_Sort(self, nums: List[int]) -> List[List[int]]:
# 元素数目小于3个,不存在解
if len(nums) < 3:
return []
# 元素个数刚好为3个,如果恰好为0,则为
if len(nums) == 3:
if sum(nums) == 0:
return [nums]
else:
return []
# 元素个数>3个
result = []
# 3层循环
nums = list(sorted(nums))
for i in range(0, len(nums)):
if i == 0 or nums[i] != nums[i - 1]:
for j in range(i + 1, len(nums)):
if j == i + 1 or nums[j] != nums[j - 1]:
for k in range(j + 1, len(nums)):
if k == j + 1 or nums[k] != nums[k - 1]:
# 判断是否和为零
if nums[i] + nums[j] + nums[k] == 0:
# 满足条件的话对三个元素进行排序,并检查在结果集合中是否已经存在该结果
temp = [nums[i], nums[j], nums[k]]
result.append(list(temp))
return result
通过分析上述代码不难发现,这种先排序的方法并不会影响时间复杂度,因为三层循环的框架依旧在。那么如何才能摆脱三层循环的限制呢?官方的题解在分析时指出,在排序之后的数组 n u m s nums nums中枚举时,由于取到的三个数字在顺序上是升序的,在选定 a , b a, b a,b之后,有唯一的 c c c满足 a + b + c = 0 a+b+c=0 a+b+c=0这一条件,那么下一次枚举时,对应找到 b ′ , c ′ b', c' b′,c′,满足上述条件,由于此时 b ′ > b b'>b b′>b,那么此时 c ′ < c c'<c c′<c,因此内层的两个循环其实可以合并为一个,这正是我们在对数组进行排序后带来的新特性。这样一来,第一层枚举在最外层,内层的两层枚举变为了同一层,时间复杂度自然从 O ( N 3 ) O(N^3) O(N3)变为 O ( N 2 ) O(N^2) O(N2),而排序带来的复杂度为 O ( N L o g N ) O(NLogN) O(NLogN),在取渐进意义时其可以忽略,整体的时间复杂度记为 O ( N 2 ) O(N^2) O(N2)。下面给出代码:
'''
既然上述方案会导致超时,只能另寻方法
基于双指针的方法,主要改进的思路在于,既然三层循环会带来O(N^3)的复杂度,那么必须想办法摒弃。
由于我们需要考虑最终集合中元素不会重复,一种简单的做法就是先对整个数组排序
在枚举过程中,如果前两个数字a, b一旦确定,那么就有唯一的c与之匹配使得a+b+c=0
而当b向右移动枚举到b'时,由于b'>b,那么满足条件的c'<c也是必然的
因此b和c的枚举可以放到同一层循环,b由小到大,c由大到小
'''
def threeSum(self, nums: List[int]) -> List[List[int]]:
if len(nums) < 3:
return []
if len(nums) == 3:
if sum(nums) == 0:
return [nums]
else:
return []
result = []
# first sort the given array with time complexity O(NlogN)
nums = list(sorted(nums))
for i in range(0, len(nums)):
# nums[i] != nums[i - 1]这一条件是为了排除连续两个相同的数字会被单一枚举对象连续用两次,从而导致重复
# 下面的所有该判断都是同一目的
if i == 0 or nums[i] != nums[i - 1]:
# 第三层枚举所用指针初始化,从后往前
k = len(nums) - 1
# 第二层枚举
for j in range(i + 1, len(nums)):
# 此处判断与上一个判断目的相同
if j == i + 1 or nums[j] != nums[j - 1]:
# 双指针在使用时需要判断两个指针的相对位置,右指针不能越过左指针
# 在指针j更新后,满足条件的k对应的索引势必比上次要小
while nums[i] + nums[j] + nums[k] > 0 and k > j:
k -= 1
# 如果k == j,此时会导致三个数中出现重复,不满足集合的特性,不能作为结果
if nums[i] + nums[j] + nums[k] == 0 and k != j:
result.append([nums[i], nums[j], nums[k]])
return result
以上是三数之和问题使用枚举方法求解的基本步骤,大家有新的方法的话可以在评论区交流(上述内容主要来自Leetcode官解,这里只是为了自己记录一下刷题过程中的心得~)