三数之和
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意: 答案中不可以包含重复的三元组。
示例:
给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为: [ [-1, 0, 1], [-1, -1, 2] ]
思路
这个题目的目标是在指定数组中找到三个不重复的和为 0 的数。分解为三个条件:
- 指定数组
- 三个数
- 不重复
- 和为 0
最容易想到的就是三个 for 循环,分别找到三个数,然后返回。但是这种方法明显效率不高。于是有了通过两个 for 循环来解决的方法。
哈希解法
根据三个数加起来等于 0 这个条件,可以推导出满足这个条件的两种情况。
- 三个数全为 0
- 三个数存在负数
根据这两个情况,可以知道,如果数组中最小值大于 0 ,那么就可以直接退出循环。于是可以先对数组进行排序,再依次取数。
对于排序后的数组进行循环,这次循环确定的是 a,即结果中第一个数。再在循环中嵌套一个循环,找到 c。
那就奇怪了,b 怎么确定呢?这个问题可以稍后解答,先对边界条件进行处理。
对于 a 来说,如果 a > 0, 那么再往后找就不能找到满足 a + b + c = 0 的三个数了。那么就可以直接退出循环。
// 伪代码 如果 a > 0,a + b + c > 0,直接退出循环
if(a > 0) {
break
}
还有一个不重复的条件,对于 a 来说,就是比较当前下标为 i 和 下标为 i - 1 的数值是否相等,如果相等就跳过循环。
// 跳过 a 重复情况
if(i > 0 && sortNums[i] === sortNums[i - 1]) {
continue
}
接下来就是确定 b 和 c 了。
上面说过,内层 for 循环确定的数字是 c,但是其实这个 for 循环确定的数字可以是 b ,也可以是 c。为了使用一个 for 循环确认两个数字,需要引入一个新的变量 set,这个 set 用来保存已循环但未处理的数字,可以理解为 a 和 c 之间的数的区间。
由上图可知,再确定 a 和 c 的情况下,可以通过 0 - (a + c) 计算出 b 的值,再在 set 中找是否存在 b ,如果存在,那么就将结果缓存并清除,不存在则将 c 放入 set,继续循环。set 中存储 a 和 c 之间的所有未处理的值。
哈希解法代码
var threeSum = function(nums) {
let result = []
// 排序
const sortNums = nums.sort((a, b) => a - b)
const len = sortNums.length
// 第一个 for 循环确定 a 数
for(let i = 0; i < len; i++) {
const set = new Set()
const a = sortNums[i]
// a < b < c
// 如果 a > 0,a + b + c > 0,直接退出循环
if(a > 0) {
break
}
// 跳过 a 重复情况
if(i > 0 && sortNums[i] === sortNums[i - 1]) {
continue
}
for(let j = i + 1; j < len; j++) {
const c = sortNums[j]
const b = 0 - (a + c)
if(set.has(b)) {
result.push([a, b, c])
while(j + 1 < len && sortNums[j + 1] === c) j++
set.delete(b)
} else {
set.add(c)
}
}
}
return result
};
双指针法
双指针法和哈希法的区别是对 a 和 c 中间区间的处理,在哈希解法中使用的 set 缓存区间数据,在双指针解法中则需要使用两个指针分别指向区间开始位置和区间结束位置。所以双指针的其他代码和哈希解法基本一致,重点在对区间的处理。
由于数组已经先进行了排序,所以区间开头和区间结尾一定的这个区间的最小值和最大值,如果这时 a + b + c > 0 则移动区间结尾指针,a + b + c < 0 则移动区间开头指针,如果等于 0 则放入结果集。
双指针法代码
var threeSum = function(nums) {
let result = []
// 排序
const sortNums = nums.sort((a, b) => a - b)
const len = sortNums.length
// 第一个 for 循环确定 a 数
for(let i = 0; i < len; i++) {
const set = new Set()
const a = sortNums[i]
// a < b < c
// 如果 a > 0,a + b + c > 0,直接退出循环
if(a > 0) {
break
}
// 跳过 a 重复情况
if(i > 0 && sortNums[i] === sortNums[i - 1]) {
continue
}
let left = i + 1
let right = len - 1
while(left < right) {
const sum = sortNums[left] + sortNums[right] + a
if(sum === 0) {
result.push([a, sortNums[left], sortNums[right]])
while(sortNums[right] === sortNums[right - 1]) right--
while(sortNums[left] === sortNums[left + 1]) left++
left++
right--
} else if(sum > 0) {
while(sortNums[right] === sortNums[right - 1]) right--
right--
} else {
while(sortNums[left] === sortNums[left + 1]) left++
left++
}
}
}
return result
};