leetcode hot 100
三数之和
描述
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] ;满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
解题思路
与day1的两数之和不同,这里没有只有一个正确答案的约束了,这就意味着hash法不好用了。
1、暴力解法,使用三重循环,复杂度O(n^3)铁超时;
2、由于不存在O(n)的解法,考虑对数组进行排序,看看在有序数组上能有什么优化空间,排序后把-nums[i]作为target,在i之后的数组中找到两数之和为target的数j和k;设置两个指针j,k分别从头部和尾部向中间移动,直到num[j]+num[k]==target为止。复杂度为O(n^2);需要考虑去重。险些超时。本题难点也在于去重,这里使用的方法是得到全部答案后用set去重,肯定有优化空间。
3、官方题解,也是排序+双指针,但是去重写的比自己的好很多。不重复的实现方式:二重循环枚举到的元素不小于当前第一重循环枚举到的元素;第三重循环枚举到的元素不小于当前第二重循环枚举到的元素。同一重循环中相邻两次枚举的元素不能相同;这样得到的三元组只会是a<b<c,排除了bac和cba等情况;
4、题解中有对官方题解优化的:只写思路,不做代码实现了
4.1 设 s = nums[first] + nums[first+1] + nums[first+2],如果 s > 0,由于数组已经排序,后面无论怎么选,选出的三个数的和不会比 s 还小,所以只要 s > 0 就可以直接 break 外层循环了。
4.2 如果 nums[first] + nums[n-2] + nums[n-1] < 0,由于数组已经排序,nums[first] 加上后面任意两个数都是小于 0 的,所以下面的双指针就不需要跑了。但是后面可能有更大的 nums[first],所以还需要继续枚举,continue 外层循环。
代码段
// Java
// 排序后双指针
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
Set<String> set = new HashSet<>();
for (int i = 0; i < nums.length; i++) {
int target = -nums[i];
// 转化成2数之和
int x = i + 1, y = nums.length - 1;
while (x < y) {
if (nums[x] + nums[y] == target) {
// find target
List<Integer> list = new ArrayList<Integer>(List.of(nums[i], nums[x], nums[y]));
String key = nums[x] + "" + nums[y];
if (!set.contains(key)) {
res.add(list);
}
set.add(key);
int temp = nums[x];
while (nums[x++] > temp);
} else if (nums[x] + nums[y] > target) {
int temp = nums[y];
while (nums[y--] < temp ;
continue;
} else if (nums[x] + nums[y] < target) {
int temp = nums[x];
while (nums[x++] > temp);
continue;
}
}
}
return res;
}
}
// 官方题解
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;
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 的指针的左侧
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;
}
}
// Kotlin
// Python
// Go
// JavaScript
// C++
// Rust
技巧总结
双指针法要牢记,还是要从使用各种条件缩小解空间的思路出发。