LeetCode15 三数之和 3Sum & 18 四数之和 4Sum 解法总结
3Sum 题目
要点:
- 可能包含多组解
- 解答不能重复,比如:[-1, 0, 1] 和 [0, -1, 1] 视为重复解
3Sum 解题
暴力遍历
没有思路的时候,可以先使用暴力遍历的方法解题,然后再思考怎么优化。平时做项目的时候也是如此,没有好思路的时候,至少先找到一个可行方案,有了备选方案后,再继续思考更优的解法。
使用暴力遍历的话,逻辑上是很简单的。
再有一个是不能有重复解,采用的方法:每次找到一组解,对这组解进行排序,并判断这组解是否已经存在,不存在的话再加进要返回的list里。
固定一个数 + 两数之和哈希解法
结合【两数之和】的解法:固定第一个数 x,接下来就是在剩余的数里找 y + z = -x,从而转化成两数之和问题。
# python
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
ret = []
if len(nums) < 3:
return ret
for i in range(len(nums) - 2):
sum2 = -nums[i]
nums_dict = {}
for j in range(i + 1, len(nums)):
if sum2 - nums[j] in nums_dict.keys():
temp = [nums[i], nums[j], sum2 - nums[j]]
if sorted(temp) not in ret:
ret.append(sorted(temp))
else:
nums_dict[nums[j]] = j
return ret
一个简单的优化:与其每次找到解的时候排序,不如一次性在最开始对整个数组进行排序。
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
ret = []
if len(nums) < 3:
return ret
nums.sort() # O(nlogn)
for i in range(len(nums) - 2): # O(n^2)
sum2 = -nums[i]
nums_dict = {}
for j in range(i + 1, len(nums)):
if nums[j] in nums_dict.keys():
temp = [nums[i], sum2 - nums[j], nums[j]]
if temp not in ret:
ret.append(temp)
else:
nums_dict[sum2 - nums[j]] = j
return ret
排序 + 双指针
官方解答非常详细,这里截取一些方法论,以后遇到类似的情况要能够联想到并应用起来!
- 为什么想到排序?
- 为什么想到用双指针?
当我们需要枚举数组中的两个元素时,如果我们发现随着第一个元素的递增,第二个元素是递减的,那么就可以使用双指针的方法。
官方的解法:
- [左指针] 每次右移一个位置,[右指针] 每次左移若干个位置
- 第一重循环枚举的 first element 与上一次不同,第二重循环枚举的 second element 与上一次不同:不会出现重复解
// javascript
var threeSum = function(nums) {
const n = nums.length;
const res = [];
if (n < 3) return res;
nums.sort((a, b) => a - b); // 升序排列
for (let i = 0; i < n - 2; i++) { // 枚举 a
if (nums[i] > 0) return res;
if (i > 0 && nums[i] === nums[i - 1]) continue; // 需要和上次枚举的数不同
const target = -nums[i];
let k = n - 1; // c 对应的指针初始指向数组的最右端
for (let j = i + 1; j < n - 1; j++) { // 枚举 b
if (j > i + 1 && nums[j] === nums[j - 1]) continue; // 需要和上次枚举的数不同
while (j < k && nums[j] + nums[k] > target) { // 需要保证 b 的指针在 c 的指针的左侧
k--;
}
// 如果指针重合,随着 b 后续的增加
// 就不会有满足 a + b + c = 0 并且 b < c 的 c 了,可以退出循环
if (j === k) break;
if (nums[j] + nums[k] === target) res.push([nums[i], nums[j], nums[k]]);
}
}
return res;
};
换一种写法:
// javascript
var threeSum = function(nums) {
const n = nums.length;
const res = [];
if (n < 3) return res;
nums.sort((a, b) => a - b);
for (let i = 0; i < n - 2; i++) {
if (i > 0 && nums[i] === nums[i - 1]) continue; // 不重复枚举 a
const target = -nums[i];
let j = i + 1, k = n - 1; // 双指针
while (j < k) {
if ((j > i + 1 && nums[j] === nums[j - 1]) || nums[j] + nums[k] < target) {
j++; // 不重复枚举 b
} else if ((k < n - 1 && nums[k] === nums[k + 1]) || nums[j] + nums[k] > target) {
k--; // 不重复枚举 c
} else {
res.push([nums[i], nums[j], nums[k]]);
j++;
k--;
}
}
}
return res;
};
可以加两个剪枝条件:
// javascript
var threeSum = function(nums) {
const n = nums.length;
const res = [];
if (n < 3) return res;
nums.sort((a, b) => a - b);
for (let i = 0; i < n - 2; i++) {
// 固定 i 后能取到的最小三数之和 > 0, 而下一次循环 a 会变得更大 => 提前知道后续不可能找到满足条件的三个数
// 因为这题比较特殊, 和取的是 0, 它是一个非负数, 提前结束的条件也可以写成:if (nums[i] > 0) break;
if (nums[i] + nums[i + 1] + nums[i + 2] > 0) break;
// 不重复枚举 a
// 固定 i 后能取到的最大三数之和 < 0 => 在当前 i 下, 不可能有 j,k 能满足条件, 直接枚举下一个 a
if (i > 0 && nums[i] === nums[i - 1] || nums[i] + nums[n - 1] + nums[n - 2] < 0) continue;
const target = -nums[i];
let j = i + 1, k = n - 1; // 双指针
while (j < k) {
if ((j > i + 1 && nums[j] === nums[j - 1]) || nums[j] + nums[k] < target) {
j++; // 不重复枚举 b
} else if ((k < n - 1 && nums[k] === nums[k + 1]) || nums[j] + nums[k] > target) {
k--; // 不重复枚举 c
} else {
res.push([nums[i], nums[j], nums[k]]);
j++;
k--;
}
}
}
return res;
};
时间复杂度:
O
(
N
2
)
O(N^2)
O(N2) -> 排序
O
(
N
l
o
g
N
)
O(NlogN)
O(NlogN),遍历两层循环
O
(
N
2
)
O(N^2)
O(N2)
空间复杂度:
O
(
l
o
g
N
)
O(logN)
O(logN) -> 我们忽略存储答案的空间,额外的排序的空间复杂度为
O
(
l
o
g
N
)
O(log N)
O(logN)。然而我们修改了输入的数组nums,在实际情况下不一定允许,因此也可以看成使用了一个额外的数组存储了nums 的副本并进行排序,空间复杂度为
O
(
N
)
O(N)
O(N)。
4Sum 题目及解题
其实是一道换汤不换药的【三数之和】题:固定第一个数 a,寻找 b + c + d = target - a -> 三数之和问题,【四数之和】和【三数之和】一样有多组解,且解不能重复。在【三数之和】问题中,我们已经分析过如何去重复,就不赘述了。
// javascript
var fourSum = function(nums, target) {
const n = nums.length;
const res = [];
if (n < 4) return res;
nums.sort((a, b) => a - b);
for (let a = 0; a < n - 3; a++) { // 固定第一个数
if (a > 0 && nums[a] === nums[a - 1]) continue; // 不能重复
for (let b = a + 1; b < n - 2; b++) { // 固定第二个数
if (b > a + 1 && nums[b] === nums[b - 1]) continue; // 不能重复
const sumTwo = target - nums[a] - nums[b];
let c = b + 1, d = n - 1;
// 双指针遍历
while (c < d) {
if ((c > b + 1 && nums[c] === nums[c - 1]) || nums[c] + nums[d] < sumTwo) {
c++;
} else if ((d < n - 1 && nums[d] === nums[d + 1]) || nums[c] + nums[d] > sumTwo) {
d--;
} else {
res.push([nums[a], nums[b], nums[c], nums[d]]);
c++; // !!!
d--; // !!!
}
}
}
}
return res;
};
来进行一个小优化,加一些限制条件:在确知无解的情况下,提前结束当前这层循环或进行该循环的下一个遍历值:
// javascript
var fourSum = function(nums, target) {
const n = nums.length;
const res = [];
if (n < 4) return res;
nums.sort((a, b) => a - b);
for (let a = 0; a < n - 3; a++) {
if (nums[a] + nums[a + 1] + nums[a + 2] + nums[a + 3] > target) break;
if ((a > 0 && nums[a] === nums[a - 1]) || nums[a] + nums[n - 1] + nums[n - 2] + nums[n - 3] < target) continue;
for (let b = a + 1; b < n - 2; b++) {
if (nums[a] + nums[b] + nums[b + 1] + nums[b + 2] > target) break;
if ((b > a + 1 && nums[b] === nums[b - 1]) || nums[a] + nums[b] + nums[n - 1] + nums[n - 2] < target) continue;
const sumTwo = target - nums[a] - nums[b];
let c = b + 1, d = n - 1;
while (c < d) {
if ( (c > b + 1 && nums[c] === nums[c - 1]) || nums[c] + nums[d] < sumTwo) {
c++;
} else if ((d < n - 1 && nums[d] === nums[d + 1]) || nums[c] + nums[d] > sumTwo) {
d--;
} else {
res.push([nums[a], nums[b], nums[c], nums[d]]);
c++;
d--;
}
}
}
}
return res;
};
时间复杂度: O ( n 3 ) O(n^3) O(n3),其中 n n n 是数组的长度。排序的时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn),枚举四元组的时间复杂度是 O ( n 3 ) O(n^3) O(n3),因此总时间复杂度为 O ( n 3 + n l o g n ) = O ( n 3 ) O(n^3+nlogn)=O(n^3) O(n3+nlogn)=O(n3)。
空间复杂度: O ( l o g n ) O(logn) O(logn),其中 n n n 是数组的长度。空间复杂度主要取决于排序额外使用的空间。此外排序修改了输入数组 nums,实际情况中不一定允许,因此也可以看成使用了一个额外的数组存储了数组 nums 的副本并排序,空间复杂度为 O ( n ) O(n) O(n)。