题目
给定一个整数数组 nums
,返回所有和为 0
且不重复的三元组。
题解(排序 + 双指针)
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
int n = nums.length;
//排序
Arrays.sort(nums);
List<List<Integer>> ans = new ArrayList<List<Integer>>();
//枚举a
for(int first = 0; first < n; first++) {
// 第一层循环的每一次枚举需要和上一次枚举的数不相同
if (first > 0 && nums[first] == nums[first - 1]) {
continue;
}
// c 对应的指针初始指向数组的最右端
int third = n - 1;
// 令 b + c = target
int target = -nums[first];
// 枚举 b
for (int second = first + 1; second < n; second++) {
// 第二层循环的每一次枚举需要和上一次枚举的数不相同
if (second > first + 1 && nums[second] == nums[second - 1]) {
continue;
}
//对于每个b,递减c
//同时需要保证 b 的指针在 c 的指针的左侧
while (second < third && nums[second] + nums[third] > target) {
--third;
}
// 如果指针重合,随着 b 后续的增加
// 就不会有满足 a+b+c=0 并且 b<c 的 c 了,可以退出循环
if (second == third) {
break;
}
if (nums[second] + nums[third] == target) {
List<Integer> list = new ArrayList<Integer>();
list.add(nums[first]);
list.add(nums[second]);
list.add(nums[third]);
ans.add(list);
}
}
}
return ans;
}
}
分析:
大框架:三重循环
枚举所有组合,时间复杂度为O(N^3)
难点:不重复
题目中要求找到所有「不重复」且和为 0 的三元组,这个「不重复」的要求使得我们无法简单地使用三重循环枚举所有的三元组。这是因为在最坏的情况下,数组中的元素全部为 0,即
[0, 0, 0, 0, 0, ..., 0, 0, 0]
任意一个三元组的和都为 0。如果我们直接使用三重循环枚举三元组,会得到 O(N3)个满足题目要求的三元组。其余较坏的情况还需要使用哈希表进行去重操作,得到不包含重复三元组的最终答案。
为保证不重复,可以先将数组中元素从小到大排序,枚举时,三元组 (a,b,c)需满足 a≤b≤c才被选中,保证了只有 (a,b,c) 这个顺序会被枚举到,而 (b,a,c)、(c,b,a)等等这些不会,这样就减少了重复。
三层循环中,对于每一种前一层循环,会依次枚举后面的元素作为第二层循环;对于每一种前两层循环的组合,会依次枚举后面的元素作为第三层循环。
所以,对于每一重循环而言,相邻两次枚举的元素不能相同,否则也会造成重复。举个例子,如果排完序的数组为
[0, 1, 2, 2, 2, 3]
^ ^ ^
我们使用三重循环枚举到的第一个三元组为 (0,1,2),如果第三重循环继续枚举下一个元素,那么仍然是三元组 (0,1,2),产生了重复。因此我们需要将第三重循环「跳到」下一个不相同的元素,即数组中的最后一个元素 3,枚举三元组 (0,1,3)。
--------------继续优化--------------
可以发现,如果我们固定了前两重循环枚举到的元素 a 和 b,那么只有唯一的 c 满足 a+b+c=0。当第二重循环往后枚举一个元素 b′ 时,由于 b' > b,那么满足 a+b′+c′ =0 的 c′ 一定有 c′<c,即 c′ 在数组中一定出现在 c 的左侧。
也就是说,我们可以从小到大枚举 b,同时从大到小枚举 c,即第二重循环和第三重循环实际上是并列的关系。有了这样的发现,我们就可以保持第二重循环不变,而将第三重循环变成一个从数组最右端开始向左移动的指针。
*************************************************************************************************************** 这个方法就是我们常说的「双指针」,当我们需要枚举数组中的两个元素时,如果我们发现随 *
* 着第一个元素的递增,第二个元素是递减的,那么就可以使用双指针的方法,将枚举的时间复 *
* 杂度 从 O(N^2)减少至 O(N)。 *
**************************************************************************************************************
为什么是 O(N)呢?这是因为在枚举的过程每一步中,「左指针」会向右移动一个位置(也就是题目中的 b),而「右指针」会向左移动若干个位置,这个与数组的元素有关,但我们知道它一共会移动的位置数为 O(N),均摊下来,每次也向左移动一个位置,因此时间复杂度为 O(N)。
总的时间复杂度:排序O(NlogN) + 枚举O(N^2) = O(N^2)
-------------值得细想的------------
//对于每个b,递减c
//同时需要保证 b 的指针在 c 的指针的左侧
while (second < third && nums[second] + nums[third] > target) {
--third;
}
// 如果指针重合,随着 b 后续的增加
// 就不会有满足 a+b+c=0 并且 b<c 的 c 了,可以退出循环
if (second == third) {
break;
}
if (nums[second] + nums[third] == target) {
... ...
}
这里设了一个target变量,不仅因为考虑到与【两数之和】问题的相似性,LeetCode小白刷题0001-热题100之两数之和(简单)-CSDN博客
还因为:
- 并不是对于每一个b都存在正确的c
- while括号中用 >target 而不是 =target : 如果用 =,在左右指针重合时只能判断对于当前这个 b没有对应的 c ,没法判断--->//如果指针重合,随着 b 后续的增加,就不会有满足 a+b+c=0 并且 b<c 的 c 了,可以退出循环。而为什么用 > 而不是 < , 因为数组是被从小到大排序的。