三数之和
题目描述
给你一个整数数组 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
求解
(1) 暴力
有五个用例超出时间限制
javascript
var threeSum = function(nums) {
// 先暴力,总得先解出来
nums.sort((a, b) => a - b)
let set = new Set()
for (let i = 0; i < nums.length - 2; i++) {
for(let j = i + 1; j < nums.length - 1; j++) {
for(let k = j + 1; k < nums.length; k++) {
if (nums[i] + nums[j] + nums[k] === 0) {
// Set 本身是基于引用地址去重的,而数组是引用类型,所以 set.add([nums[i], nums[j], nums[k]]) 无法正确去重
// 将 数组转为字符串,并使用逗号隔开
let item = [nums[i], nums[j], nums[k]].join(',')
set.add(item)
}
}
}
}
// 返回的时候需要将 字符串 按照逗号 分割并转为数字类型
let ans = Array.from(set).map(str => str.split(',').map(Number))
return ans
};
不足:对js一些语法用的还是不够熟练;还是要多学一些优化策略。
三重循环暴力解法虽然思路清晰,但时间复杂度是 O(n3)O (n^3)O(n3),在数据量大的时候会超时。这里有一个基于 "双指针" 的优化思路,可以将时间复杂度降低到(n2)(n^2)(n2),并且天然地避免了使用 Set 去重,效率更高。
(2)双指针
先做一个两数之和的题目,如下:
两数之和 II - 输入有序数组
-
题目描述:
给定一个已按照 升序排列 的整数数组 numbers ,请你从数组中找出两个数满足相加之和等于目标数 target 。
函数应该以长度为 2 的整数数组的形式返回这两个数的下标值。numbers 的下标 从 0 开始计数 ,所以答案数组应当满足 0 <= answer[0] < answer[1] < numbers.length 。
假设数组中存在且只存在一对符合条件的数字,同时一个数字不能使用两次。
示例 1:
输入:numbers = [1,2,4,6,10], target = 8
输出:[1,3]
解释:2 与 6 之和等于目标数 8 。因此 index1 = 1, index2 = 3 。
示例 2:
输入:numbers = [2,3,4], target = 6
输出:[0,2]
示例 3:
输入:numbers = [-1,0], target = -1
输出:[0,1]
提示:
2 <= numbers.length <= 3 * 104
-1000 <= numbers[i] <= 1000
numbers 按 非递减顺序 排列
-1000 <= target <= 1000
仅存在一个有效答案
-
求解:
这个题目 首先已经排好序了,关键就在于:双指针,分别指向最小值和最大值;最大的和最小的相加,比target大的话说明 最大的太大了,应该往前移动;比target小的话,说明最小的太小了,需要往后移动
javascriptvar twoSum = function(nums, target) { let left = 0 right = nums.length - 1 while (left < right) { let sum = nums[left] + nums[right] if (sum === target) return [left, right] if (sum < target) { // 最大的加最小的都比target小。最小的应该 右移 left++ } else { // 最大的加最小的 比target大,最大的应该 左移 right-- } } return [] };
那么回到三数之和这道题目中,首先进行排序,然后 先找一个,再用双指针找其他两个。思路如下:
- 先对数组进行升序排序。
- 固定第一个数 nums[i]。
- 用两个指针 left 和 right 分别指向 i 后面的两端(left = i + 1, right = nums.length - 1)。
- 计算三数之和 sum = nums[i] + nums[left] + nums[right]。
- 如果 sum === 0,则找到一个解。为了避免重复,需要同时移动 left 和 right 指针,并跳过所有相同的元素。
- 如果 sum < 0,说明需要更大的数,将 left 指针右移。
- 如果 sum > 0,说明需要更小的数,将 right 指针左移。
代码如下:
javascript
var threeSum = function(nums) {
// 排序
nums.sort((a, b) => a - b)
let ans = []
// 遍历
for (let i = 0; i < nums.length - 2; i++) {
// 固定i,找j、k
if (i > 0 && nums[i] === nums[i - 1]) continue
if (nums[i] > 0) break
let j = i + 1
k = nums.length - 1
while(j < k) {
if (nums[j] + nums[k] + nums[i] === 0) {
// 找到符合条件的
ans.push([nums[i], nums[j], nums[k]])
// 跳过所有相同的left和right,避免重复结果
while (j < k && nums[j] === nums[j + 1]) j++
while (j < k && nums[k] === nums[k - 1]) k--
j++
k--
} else if (nums[j] + nums[k] < -nums[i]) {
// -nums[i] 相当于是target
j++
} else {
k--
}
}
}
return ans
};