15. 三数之和
15. 三数之和
给你一个整数数组 nums
,判断是否存在三元组 [nums[i], nums[j], nums[k]]
满足 i != j、i != k 且 j != k
,同时还满足 nums[i] + nums[j] + nums[k] == 0
。请你返回所有和为 0
且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
示例 2:
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。
示例 3:
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。
提示:
- 3 < = n u m s . l e n g t h < = 3000 3 <= nums.length <= 3000 3<=nums.length<=3000
- − 1 0 5 < = n u m s [ i ] < = 1 0 5 -10^5 <= nums[i] <= 10^5 −105<=nums[i]<=105
思路:
哈希解法
两层for
循环就可以确定a
和b
的数值了,可以使用哈希法来确定0-(a+b)
是否在 数组里出现过,其实这个思路是正确的,但是我们有一个非常棘手的问题,就是题目中说的不可以包含重复的三元组。
把符合条件的三元组放进[][]int
切片中,然后再去重,这样是非常费时的,很容易超时。
去重的过程不好处理,有很多小细节,如果在面试中很难想到位。
时间复杂度可以做到 O ( n 2 ) O(n^2) O(n2),但还是比较费时的,因为不好做剪枝操作。
双指针
其实这道题目使用哈希法并不十分合适
,因为在去重的操作中有很多细节需要注意,在面试中很难直接写出没有bug
的代码。
接下来我来介绍另一个解法:双指针法,这道题目使用双指针法 要比哈希法高效一些,那么来讲解一下具体实现的思路。
拿这个nums
数组来举例,首先将数组排序
,然后有一层for
循环,i
从下标0
的地方开始,同时定一个下标left
定义在i+1
的位置上,定义下标right
在数组结尾的位置上。
依然还是在数组中找到 abc
使得a + b +c =0
,我们这里相当于 a = nums[i],b = nums[left],c = nums[right]
。
接下来如何移动left
和right
呢, 如果nums[i] + nums[left] + nums[right] > 0
就说明 此时三数之和大了,因为数组是排序后了,所以right
下标就应该向左移动,这样才能让三数之和小一些。
如果nums[i] + nums[left] + nums[right] < 0
说明 此时 三数之和小了,left
就向右移动,才能让三数之和大一些,直到left
与right
相遇为止。
时间复杂度: O ( n 2 ) O(n^2) O(n2)。
去重逻辑的思考
a的去重
说到去重,其实主要考虑三个数的去重。 a, b ,c
, 对应的就是 nums[i],nums[left],nums[right]
a
如果重复了怎么办,a
是nums
里遍历的元素,那么应该直接跳过去。
但这里有一个问题,是判断 nums[i] 与 nums[i + 1]
是否相同,还是判断 nums[i]
与 nums[i-1]
是否相同。
有同学可能想,这不都一样吗。
其实不一样!
都是和 nums[i]
进行比较,是比较它的前一个,还是比较它的后一个。
如果我们的写法是 这样:
if nums[i] == nums[i + 1] { // 去重操作
continue
}
那我们就把 三元组中出现重复元素的情况直接pass
掉了。 例如{-1, -1 ,2}
这组数据,当遍历到第一个-1
的时候,判断 下一个也是-1
,那这组数据{-1, -1 ,2}
就pass
了,但实际上{-1, -1 ,2}
是符合和为0
的一个三元组。
我们要做的是 不能有重复的三元组,但三元组内的元素是可以重复的!
所以这里是有两个重复的维度。
那么应该这么写:
if i > 0 && nums[i] == nums[i - 1] {
continue
}
这么写就是当前使用 nums[i]
,我们判断前一位是不是一样的元素,在看 {-1, -1 ,2}
这组数据,当遍历到 第一个 -1
的时候,只要前一位没有-1
,那么{-1, -1 ,2}
这组数据一样可以收录到 结果集里。
这是一个非常细节的思考过程。
b与c的去重
b
和c
也要去除重复的组合 例如【-1,0,1,1,2】
,那么【-1,0,1】
就是一个结果,而第二个1
得到的还是【-1,0,1】
,故让left++
, 让b
跳过第二个1
。
for left < right {
if base + nums[left] + nums[right] == 0 {
res = append(res,[]int{base,nums[left],nums[right]})
// 上面找到一个组合后,以nums[i]作为基数的可能还有其他组合,此时移动left和right
//同样,left和right也要去除重复的组合 例如【-1,0,1,1,2】,那么【-1,0,1】就是一个结果,而第二个1得到的还是【-1,0,1】,故让left++,跳过第二个1
for left < right && nums[left + 1] == nums[left] { left++ }
for left < right && nums[right - 1] == nums[right] { right-- }
//当上面两个for结束时,说明num[left+1] != nums[left],nums[right-1]!=nums[right],此时我们要取的就是left+1和right-1的那两个数,即如下
left++
right--
} else if base + nums[left] + nums[right] < 0 {
left++
} else {
right--
}
}
当然也可以用回溯法,找出所有符合条件的三元组,但是回溯法和暴力法直接三层遍历一样,时间复杂度是 O ( n 3 ) O(n^3) O(n3)。
Go代码
func threeSum(nums []int) [][]int {
/*对切片先进行排序,方便去除重复的结果,然后固定一个数,移动另外两个数*/
if nums == nil { return nil}
var res [][]int
sort.Ints(nums)
//由于是三个数之和,而另外的两个数都是取i之后的,因为取i之前的情况的三个数的组合已经包含在i还较小时作为固定数的循环中了
// 从头至尾以当前数字作为固定的数字,至少需要三个数字,所以遍历到len(nums)-2即可
for i := 0;i < len(nums) - 2;i++ {
if i > 0 && nums[i] == nums[i - 1] {continue} // 对基数去除重复的情况
base := nums[i] // 固定一个基数
left ,right := i + 1, len(nums) - 1 // 双指针的两个数
for left < right {
if base + nums[left] + nums[right] == 0 {
res = append(res,[]int{base,nums[left],nums[right]})
// 上面找到一个组合后,以nums[i]作为基数的可能还有其他组合,此时移动left和right
//同样,left和right也要去除重复的组合 例如【-1,0,1,1,2】,那么【-1,0,1】就是一个结果,而第二个1得到的还是【-1,0,1】,故让left++,跳过第二个1
for left < right && nums[left + 1] == nums[left] { left++ }
for left < right && nums[right - 1] == nums[right] { right-- }
//当上面两个for结束时,说明num[left+1] != nums[left],nums[right-1]!=nums[right],此时我们要取的就是left+1和right-1的那两个数,即如下
left++
right--
} else if base + nums[left] + nums[right] < 0 {
left++
} else {
right--
}
}
}
return res
}