题意描述:
给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例:
给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[
[-1, 0, 1],
[-1, -1, 2]
]
解题思路:
Alice: 这个三重循环肯定会超时吧。
Bob: 那肯定的,O(n^2)
都够呛,而且去重应该没那么简单。如果先把所有的答案都求出来,然后再去重可能会不行哦。求解的时候有重复计算,去重的时候还要再附加计算,可能会超时啊。
Alice: emmm, 有数组不能先排个序吗,然后再分析看看,要求 a + b + c == 0
那肯定得有正有负吧。
Bob: 不一定哦,万一是 三个零 [0, 0, 0]
呢。
Alice: 那就除了 三个零的情况,至少得有一个正数,至少得有一个负数。
Bob: 我们可以在外层遍历整个排序后的数组, 然后指定最外层遍历的是三元组中的最小值,这样就只遍历 负数 的部分就可以了。
Alice: 对对对,如果三元组中最小值是正数这样的三元组是不成立的,如果最小值是零,刚才已经处理过了。
Bob: 然后我们用双指针来 寻找 三元组中的 中间值和 最大值, 中间值一定要小于等于最大值。
Alice: 也就是说 两个指针 left < right
是一定成立的,然后还有去重的问题,无论是最小值,中间值,最大值的都要去重。
Bob: 对,最小值用 pre
记录上一次访问的值,然后比较呗,如果再遇到就跳过呗。
Alice: 然后就是 left
和 right
, 如果 nums[minIndex] + nums[left] + nums[right] == 0
,然后left
和 right
要向中间移动,一直移动到 两个新的元素为止。
Bob: 如果 nums[minIndex] + nums[left] + nums[right] < 0
就 left
右移,否则 right
左移,应该就可以了。
Alice; 那应该就没错了,最小值,中间值,最大值的去重都考虑了,时间复杂度是O(n^2)
,空间复杂度应该是O(1)
,还不错。
Bob: 😎😎
###########################################
Alice: 我们好像想复杂了,不过上面的思路还是在正确的方向上的。
Bob: 你有新想法了 ?
Alice: 对,首先给数组排序,然后循环遍历整个数组,这个最外层的循环遍历的是所有满足条件三元组的最小值,这个最小值应该是无重复的。
Bob: 等一下,如果输入是 [-2,0,1,1,2]
答案中的 [-2,0,2], [-2,1,1]
,两个三元组的最小值不就是重复的吗 ?
Alice: 不是这样理解的,-2 在最外层的循环中应该是无重复的,后面的 [0,2]
和 [1,1]
都是在内层循环中找到的。你听我说完先,假设外层循环到了 num[i]
内层循环的任务是找到 数组中 所有 和为-num[i]
的二元组,这个可以用双指针去做呀,前面已经给数组排过序了,只需要在 i+1, num.length-1
的范围去找就可以了。然后找到一组后,为了防止有重复的 二元组,两边都要分别跳过相等的元素。这样应该就可以了。
Bob: 好像可以啊,应该还有一写边界条件可以特殊处理一下加速代码。
Alice: 我发现我们之前的代码基本上就是这个意思,就是有点不规范的地方。
Bob: 😲
2023 年7月9日新增解法:排序 + 二分查找
Alice: 还是要多读题呀,题目中说
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0
i j k 是互不相等的,我们把 nums 排序之后,i j k 这些下标之间的关系应该满足 i < j < k,这是满足 i j k 不互相等的条件吧。然后还有返回的三元组不能重复,也就是说 i 和 j 对应的取值不应该有重复的,因为所有的三元组都是 [ i 处的 value,j 处的value,k 处的value ] 的形式,所以 i 和 j 处的 value 不应该有重复,而且只有 i j 的 value 不重复,k 处的 value 也不会重复。
Bob: 说的对,不过二分查找的时候还可以优化。如果 i j 的 value 之和是一个负数,二分查找的开始坐标应该是最小的正数。
Alice: 说的不错,说到正负数,还需要处理一下边界情况。比如说全是正数,全是负数的情况。另外你说的优化也应该考虑到万一没有正数的情况。
Bob: All right
代码:
JavaScript: 排序 + 二分查找
/**
* @param {number[]} nums
* @return {number[][]}
*/
var threeSum = function(nums) {
// 升序排列
const sortNums = nums.sort((a ,b) => a - b);
// 最小值大于 0
if (sortNums[0] > 0) {
return [];
}
// 最大值小于 0
if (sortNums[sortNums.length - 1] < 0) {
return [];
}
// 在 sortNums 中二分查找
const binarySearch = (start, end, target) => {
while(start <= end) {
let middle = Math.floor((start + end) / 2);
let val = sortNums[middle];
if (val === target) {
return middle;
}
if (val > target) {
end = middle - 1;
}
if (val < target) {
start = middle + 1;
}
}
return -1;
}
const postiveStartIndex = sortNums.findIndex(item => item > 0);
const result = [];
for (let i=0; i<sortNums.length; ++i) {
// 去重复
if(i > 0 && sortNums[i-1] === sortNums[i]) {
continue;
}
for(let j=i+1; j<sortNums.length; ++j) {
// 去重复
if(j > i+1 && sortNums[j-1] === sortNums[j]) {
continue;
}
const twoSum = sortNums[i] + sortNums[j];
// 一定找不到
if (twoSum < 0 && postiveStartIndex === -1) {
continue;
}
let startIndex = j + 1;
if (twoSum < 0 && postiveStartIndex > startIndex) {
startIndex = postiveStartIndex;
}
const endIndex = sortNums.length - 1;
const targetIndex = binarySearch(startIndex, endIndex, -1 * twoSum);
if (targetIndex !== -1) {
result.push([
sortNums[i],
sortNums[j],
sortNums[targetIndex]
]);
}
}
}
return result;
};
JavaScript 排序 + 双指针,可能是更好理解的版本
/**
* @param {number[]} nums
* @return {number[][]}
*/
var threeSum = function(nums) {
// 升序排列
nums.sort((a, b) => a - b);
console.log('nums', nums);
const maxx = nums[nums.length - 1];
const minn = nums[0];
// 边界条件
if (minn > 0 || maxx < 0) {
return [];
};
const res = [];
for (let i=0; i<=nums.length-3; i++) {
// 去重
if (i > 0 && nums[i] === nums[i-1]) {
continue;
}
// “剪枝” 优化
if (nums[i] > 0) {
continue;
}
let left = i + 1;
let right = nums.length - 1;
while (left < right) {
const tempRes = nums[i] + nums[left] + nums[right];
if (tempRes === 0) {
res.push([nums[i], nums[left], nums[right]]);
left++;
right--;
// 去重
while (left < nums.length && nums[left] === nums[left - 1]) {
left++;
}
while (right >= 0 && nums[right] === nums[right + 1]) {
right--;
}
}
if (tempRes < 0) {
left++;
}
if (tempRes > 0) {
right--;
}
}
}
return res;
};
JavaScript: 排序 + 双指针
/**
* @param {number[]} nums
* @return {number[][]}
*/
var threeSum = function(num) {
if(!num || !Array.isArray(num) || num.length === 0){
return [];
}
num.sort((a, b) => {return a-b});
const len = num.length;
// 直接处理边界值输入
if(num[len-1] < 0){
return [];
}
if(num[0] > 0){
return [];
}
let ret = [],
pre = NaN;
for(let i=0; i<len; ++i){
// 最外层循环的是 所有答案中的最小值,最小值应该是无重复的
if(num[i] === pre){
continue;
}
if(num[i] > 0){
break;
// num[i]以及后面的数字都是正数
}else if(num[i] === 0){
pre = 0;
// try to find the other zero
// and there are only one ans for all zeroes
if(i+2 < len && num[i+2] === 0){
ret.push([0,0,0]);
}
}else{
pre = num[i];
// 找出所有可以与 num[i] 相加为 0 的无重复组合
let tmp = getTarget(num, -num[i], i+1, len-1);
if(tmp.length){
tmp.forEach(item => {
ret.push([num[i], item[0], item[1]]);
});
}
}
}
return ret;
};
function getTarget(num, target, i, j){
let ret = []
while(i < j){
if(num[i] + num[j] > target){
j--;
}else if(num[i] + num[j] < target){
i++;
}else{
ret.push([num[i], num[j]]);
let tmp = num[i];
// 找到一组之后要“去重”
while(tmp === num[i]){
i++;
}
tmp = num[j];
while(tmp === num[j]){
j--;
}
}
}
return ret;
}
Python 方法一: 排序 + 双指针。
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
ans = []
if len(nums) < 3:
return ans
else:
nums.sort()
positiveIndex = -1
zeroCnt = 0
for x in range(len(nums)):
if nums[x] == 0:
zeroCnt += 1
elif nums[x] > 0:
positiveIndex = x
break
#print(nums)
#print(zeroCnt)
if positiveIndex == -1:
# 没有正数
if zeroCnt >= 3:
return [[0,0,0]]
else:
return []
pre = nums[0] + 1
# pre 初始值不等于 nums[0] 即可
for x in range(len(nums)):
# 遍历 所有可能的三元组中最小的那一个
if nums[x] > 0:
# nums[x] > 0, x 后的所有数字都是正数
continue
elif nums[x] < 0:
if nums[x] == pre:
# 去重, 两个最小值
continue
else:
# 寻找满足条件的三元组
pre = nums[x]
positiveOne = len(nums)-1
# positiveOne 是三元组中最大的 元素的下标
anotherOne = x + 1
while anotherOne < positiveOne:
if nums[x] + nums[anotherOne] + nums[positiveOne] == 0:
ans.append([nums[x], nums[anotherOne], nums[positiveOne]])
# 去重
while positiveOne > anotherOne and nums[positiveOne] == nums[positiveOne-1]:
positiveOne -= 1
while anotherOne < positiveOne and nums[anotherOne] == nums[anotherOne+1]:
anotherOne += 1
# 尝试新的答案
positiveOne -= 1
anotherOne += 1
elif nums[x] + nums[anotherOne] + nums[positiveOne] < 0:
anotherOne += 1
else:
positiveOne -= 1
else:
# nums[x] == 0, 且 0 是三元组中的最小值
if zeroCnt >= 3:
# 最后一组三元组
ans.append([0,0,0])
break
return ans
Java 方法一: 排序 + 双指针。
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> ans = new ArrayList(); // ans 存储答案
if(nums.length < 3){ // 不可能有满足条件的三元组,直接返回
return ans;
}else{
Arrays.sort(nums); // 排个序
int zeroCnt = 0;
int firstPositiveIndex = -1; // 统计下数组中零的个数 和 第一个正数出现的下标
for(int i=0; i<nums.length; ++i){
if(firstPositiveIndex == -1 && nums[i] > 0){
firstPositiveIndex = i;
break;
}
if(nums[i] == 0){
zeroCnt += 1;
}
}
if(firstPositiveIndex == -1){ // 一个正数也没有就可以直接返回了
// nothing, 直接到最后加零走人
}else{
int pre = nums[0] + 1; // 去重用,pre 初始值和 nums[0] 不同即可
for(int minIndex = 0; minIndex < nums.length; ++minIndex){ // 以三元组中最小的元素开始搜索
if(nums[minIndex] >= 0){
break; // 后面是没有答案的,可以直接退出循环了
}else{
if(pre == nums[minIndex]){ // 去重,跳过这个最小元素
continue;
}
pre = nums[minIndex];
int left = minIndex + 1;
int right = nums.length - 1; // right 指向三元组最大的一个数,必须是正数
while(left < right){
if(nums[minIndex] + nums[left] + nums[right] == 0){
List<Integer> tmp = new ArrayList();
tmp.add(nums[minIndex]);
tmp.add(nums[left]);
tmp.add(nums[right]);
ans.add(tmp);
while(right > left && nums[right] == nums[right-1]){
right -= 1; // 去重,最大值不能重复
}
while(left < right && nums[left] == nums[left+1]){
left += 1; // 去重,中间值不能重复
}
left += 1; // 一直到 left 和 right 指向下两个不一样的值
right -= 1;
}else if(nums[minIndex] + nums[left] + nums[right] < 0){
left += 1;
}else{
right -= 1;
}
}
}
}
}
if(zeroCnt >= 3){
List<Integer> zeroAns = new ArrayList(); // 准备三个零的答案
zeroAns.add(0);
zeroAns.add(0);
zeroAns.add(0);
ans.add(zeroAns);
}
return ans;
}
}
}
易错点:
- 一些测试用例:
[-1,0,1,2,-1,-4]
[-1,-2,-3,0,0,0]
[-4,2,2,2,1,3,0,0,0,4]
[-2,-2,-4,0,0,0,1,1,2,2,4]
[0,2,3,3]
[-1,-2]
[2,3,4,5]
- 答案:
[[-1,-1,2],[-1,0,1]]
[[0,0,0]]
[[-4,0,4],[-4,1,3],[-4,2,2],[0,0,0]]
[[-4,0,4],[-4,2,2],[-2,-2,4],[-2,0,2],[-2,1,1],[0,0,0]]
[]
[]
[]
总结:
- 笔试,面试常考。
- leetcode 题目地址
- 牛客网题目地址