题目

给你一个整数数组 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 <= nums.length <= 3000
  • -105 <= nums[i] <= 105

思路

最直接的方法就是三重循环,但是时间复杂度为 O(n^3)。考虑可以使用双指针法来优化,也就是确定第一个数之后,用双指针在同一个循环里查找另外两个数。而要使用双指针,肯定要先对数组进行排序,这样可以更方便地跳过重复的元素。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
func threeSum(nums []int) [][]int {
sort.Ints(nums)
ans := [][]int{}
for i := 0; i < len(nums)-2; i++ {
if nums[i] > 0 {
break
}
if i > 0 && nums[i] == nums[i-1] {
continue
}

for j, k, x := i+1, len(nums)-1, nums[i]+nums[i+1]+nums[len(nums)-1]; j < k; x = nums[i] + nums[j] + nums[k] {
flagJ, flagK := false, false
if x == 0 {
ans = append(ans, []int{nums[i], nums[j], nums[k]})
flagJ = true
flagK = true
} else if x > 0 {
flagK = true
} else {
flagJ = true
}
if flagJ {
for tmp := nums[j]; j < k; j++ {
if tmp != nums[j] {
break
}
}
}
if flagK {
for tmp := nums[k]; j < k; k-- {
if tmp != nums[k] {
break
}
}
}
}
}
return ans
}

由于题目中表示不可以包含重复的三元组,且每个元素只能使用一次,所以在确定每一个元素时,都需要检查是否与前一个元素相同,如果相同则跳过。这样可以避免重复的三元组。

上面的代码中,首先对数组进行排序,然后使用一个循环来确定第一个数。在第一个循环里通过 if i > 0 && nums[i] == nums[i-1] 来跳过重复的元素。接着使用双指针法来查找另外两个数,双指针分别从当前数的下一个位置和数组的最后一个位置开始,计算三数之和。如果三数之和为 0,则将三元组添加到结果中,并移动双指针跳过重复的元素。移动双指针时,也分别对左右指针使用 for 循环来跳过重复的元素,确保每个三元组都是唯一的。

题目

给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):

0 <= a, b, c, d < n
a、b、c 和 d 互不相同
nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。

示例 1:

输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]

示例 2:

输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]

提示:

  • 1 <= nums.length <= 200
  • -109 <= nums[i] <= 109
  • -109 <= target <= 109

思路

四数之和的思路与三数之和类似,也是使用双指针法。首先对数组进行排序,然后使用两个循环来确定前两个数,接着使用双指针法来查找另外两个数。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
func fourSum(nums []int, target int) [][]int {
sort.Ints(nums)
ans := [][]int{}
for a := 0; a < len(nums)-3; a++ {
if a > 0 && nums[a] == nums[a-1] {
continue
}
for b := a + 1; b < len(nums)-2; b++ {
if b > a+1 && nums[b] == nums[b-1] {
continue
}
rem := target - nums[a] - nums[b]
if nums[b+1]+nums[b+2] > rem {
break
}
if nums[len(nums)-1]+nums[len(nums)-2] < rem {
continue
}

for c, d, x := b+1, len(nums)-1, nums[b+1]+nums[len(nums)-1]; c < d; x = nums[c] + nums[d] {
flagC, flagD := false, false
if rem == x {
ans = append(ans, []int{nums[a], nums[b], nums[c], nums[d]})
flagC = true
flagD = true
} else if rem > x {
flagC = true
} else {
flagD = true
}
if flagC {
for numc := nums[c]; numc == nums[c]; c++ {
if c == len(nums)-1 {
break
}
}
}
if flagD {
for numd := nums[d]; numd == nums[d]; d-- {
if d == 0 {
break
}
}
}
}
}
}

return ans
}

仍然需要注意跳过重复的元素。首先对数组进行排序,然后使用两个循环来确定前两个数。在第一个循环里通过 if a > 0 && nums[a] == nums[a-1] 来跳过重复的 a 元素,第二个循环里通过 if b > a+1 && nums[b] == nums[b-1] 来跳过重复的 b 元素。接着使用双指针法来查找另外两个数,双指针分别从当前数的下一个位置和数组的最后一个位置开始,计算四数之和。如果四数之和等于目标值,则将四元组添加到结果中,并移动双指针跳过重复的元素。移动双指针时,也分别对左右指针使用 for 循环来跳过重复的元素,确保每个四元组都是唯一的。

上面的代码中还对一些情况做了裁剪:

  • nums[b+1]+nums[b+2] > rem 时,说明当前的 b 位置已经无法找到合适的 cd 了,直接跳出循环;
  • nums[len(nums)-1]+nums[len(nums)-2] < rem 时,说明当前的 b 位置也无法找到合适的 cd 了,直接跳过当前的 b 位置,继续下一个 b 位置。

总结

这两个题,本质都是通过双指针来优化查找有序数组中满足条件的两个元素。而排序的时间复杂度为 O(log n),所以整体的时间复杂度相较于直接用嵌套循环查找两个元素的 O(n^2),是有很大提升的。

另外这两题都需要注意重复元素的跳过处理,以确保结果中不包含重复的三元组或四元组。通过对数组进行排序,可以更方便地跳过重复元素,从而提高算法的效率。