两数之和可谓力扣上非常经典的一道题,对于计算机大牛来说,这道题与1+1=2没有什么区别,对于新手来说,这是对原本陌生算法的第一次亲密接触。
自然而然,两数之和衍生出三数之和,四数之和等众多题目,只要我们找到他们之中的本质思想,在加一点点知识储备,这种问题就不足为惧了。
注:本文为代码随想录学习笔记,代码部分来源自代码随想录
. - 力扣(LeetCode)
因为题目比较简单,所以方法选择上也比较随意。
1.双指针
class Solution {
public:
struct node//要想返回排序前的下标,只能用一个数据结构保存值对应的下标(map)
{
int id, x;
friend bool operator < (const node &n1, const node &n2)
//为了使用双指针,不惜再写一个map结构并重载<
{
return n1.x < n2.x;
}
};
vector twoSum(vector& nums, int target) {
vectortmp;
int n = nums.size();
for(int i = 0;i < n;++i)
tmp.push_back((node){i, nums[i]});
sort(tmp.begin(), tmp.end());//默认使用友元函数的‘<’
int l = 0, r = n-1;
while(l < r)
{
if(tmp[l].x+tmp[r].x == target) break;
else if(tmp[l].x+tmp[r].x < target) l++;
else r--;
}
return vector{tmp[l].id, tmp[r].id};
}
};
虽然双指针很好理解,但是这题天然对双指针不友好,因为需要排序(双指针是肯定要排序的)前的信息,因此还要再创建一个类似map的数据结构。
2.哈希表
class Solution {
public:
vector twoSum(vector& nums, int target) {
vector ans;
std::unordered_map dict;
for(int i=0;isecond,i};
}
dict.insert(pair(nums[i],i));
}
return {};
}
};
用target-element作为索引,构建哈希表。难点(这算?)在于数据结构unordered_map的相关操作
. - 力扣(LeetCode)
三数之和的本质在于在原算法基础上加了一层循环。
for(int k=0;k
虽然仅仅是加了一层循环,但是这道题的梳理难度却增加了很多
1.双指针
class Solution {
public:
vector> threeSum(vector& nums) {
vector> result;
sort(nums.begin(), nums.end());
// 找出a + b + c = 0
// a = nums[i], b = nums[left], c = nums[right]
for (int i = 0; i < nums.size(); i++) {
// 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
if (nums[i] > 0) {
return result;
}
// 错误去重a方法,将会漏掉-1,-1,2 这种情况
/*
if (nums[i] == nums[i + 1]) {
continue;
}
*/
// 正确去重a方法
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.size() - 1;
while (right > left) {
// 去重复逻辑如果放在这里,0,0,0 的情况,可能直接导致 right<=left 了,从而漏掉了 0,0,0 这种三元组
/*
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
*/
if (nums[i] + nums[left] + nums[right] > 0) right--;
else if (nums[i] + nums[left] + nums[right] < 0) left++;
else {
result.push_back(vector{nums[i], nums[left], nums[right]});
// 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
// 找到答案时,双指针同时收缩
right--;
left++;
}
}
}
return result;
}
};
2.哈希表
从三数之和开始,哈希表便因为剪枝操作复杂,占用空间大而丧失了它的优越性,后续不建议考虑
class Solution {
public:
vector> threeSum(vector& nums) {
vector> result;
sort(nums.begin(), nums.end());
// 找出a + b + c = 0
// a = nums[i], b = nums[j], c = -(a + b)
for (int i = 0; i < nums.size(); i++) {
// 排序之后如果第一个元素已经大于零,那么不可能凑成三元组
if (nums[i] > 0) {
break;
}
if (i > 0 && nums[i] == nums[i - 1]) { //三元组元素a去重
continue;
}
unordered_set set;
for (int j = i + 1; j < nums.size(); j++) {
if (j > i + 2
&& nums[j] == nums[j-1]
&& nums[j-1] == nums[j-2]) { // 三元组元素b去重
continue;
}
int c = 0 - (nums[i] + nums[j]);
if (set.find(c) != set.end()) {
result.push_back({nums[i], nums[j], c});
set.erase(c);// 三元组元素c去重
} else {
set.insert(nums[j]);
}
}
}
return result;
}
};
. - 力扣(LeetCode)
相比上一问,这道题继续增加了一重循环,并且target从常量0改为了变量。
至此,n数之和的模板化与套路逐渐清晰起来。。。
class Solution {
public:
vector> fourSum(vector& nums, int target) {
vector> result;
sort(nums.begin(), nums.end());
for (int k = 0; k < nums.size(); k++) {
// 剪枝处理
if (nums[k] > target && nums[k] >= 0) {
break; // 这里使用break,统一通过最后的return返回
}
// 对nums[k]去重
if (k > 0 && nums[k] == nums[k - 1]) {
continue;
}
for (int i = k + 1; i < nums.size(); i++) {
// 2级剪枝处理
if (nums[k] + nums[i] > target && nums[k] + nums[i] >= 0) {
break;
}
// 对nums[i]去重
if (i > k + 1 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.size() - 1;
while (right > left) {
// nums[k] + nums[i] + nums[left] + nums[right] > target 会溢出
if ((long) nums[k] + nums[i] + nums[left] + nums[right] > target) {
right--;
// nums[k] + nums[i] + nums[left] + nums[right] < target 会溢出
} else if ((long) nums[k] + nums[i] + nums[left] + nums[right] < target) {
left++;
} else {
result.push_back(vector{nums[k], nums[i], nums[left], nums[right]});
// 对nums[left]和nums[right]去重
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
// 找到答案时,双指针同时收缩
right--;
left++;
}
}
}
}
return result;
}
};
这里出现了所谓“剪枝”操作,其实就是通过条件预先排除一些不可能的情况,实现时间复杂度的小幅度优化,不过剪枝不能从根本上减小时间复杂度。
把这个题改成“n数之和”,输入int n和vector
为方便测试,依此写一个四数之和作为应用示例(在四数之和提交):
请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a、b、c 和 d 互不相同
nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案
(力扣上好像没有这题。。。)
class Solution {
public:
vector> nSum(int n, vector& nums, int start, long long target) {
vector> result;
int size = nums.size();
// 处理两数之和的情况,使用双指针法
if (n == 2) {
int left = start, right = size - 1;
while (left < right) {
long long sum = (long long)nums[left] + nums[right];
if (sum == target) {
result.push_back({nums[left], nums[right]});
// 跳过重复元素
while (left < right && nums[left] == nums[left + 1]) ++left;
while (left < right && nums[right] == nums[right - 1]) --right;
++left;
--right;
} else if (sum < target) {
++left;
} else {
--right;
}
}
} else {
// 递归处理 n > 2 的情况
for (int i = start; i < size - n + 1; ++i) {
// 跳过重复元素
if (i > start && nums[i] == nums[i - 1]) continue;
// 剪枝:如果当前最小的 n 数之和都大于 target,则退出循环
if ((long long)nums[i] + (long long)nums[i + 1] * (n - 1) > target) break;
// 剪枝:如果当前最大的 n 数之和都小于 target,则继续下一个 i
if ((long long)nums[i] + (long long)nums[size - 1] * (n - 1) < target) continue;
vector> subResult = nSum(n - 1, nums, i + 1, target - nums[i]);
for (auto& vec : subResult) {
vec.insert(vec.begin(), nums[i]);
result.push_back(vec);
}
}
}
return result;
}
vector> fourSum(vector& nums, int target) {
sort(nums.begin(), nums.end());
return nSum(4, nums, 0, target);
}
};
对与从2到n的迭代,有时候在代码量过大,逻辑难以实现的情况下,递归绝对是值得考虑的一个方向。
思考:能不能把递归改造成迭代?
. - 力扣(LeetCode)
class Solution {
public:
int fourSumCount(vector& A, vector& B, vector& C, vector& D) {
unordered_map umap; //key:a+b的数值,value:a+b数值出现的次数
// 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中
for (int a : A) {
for (int b : B) {
umap[a + b]++;
}
}
int count = 0; // 统计a+b+c+d = 0 出现的次数
// 再遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就把map中key对应的value也就是出现次数统计出来。
for (int c : C) {
for (int d : D) {
if (umap.find(0 - (c + d)) != umap.end()) {
count += umap[0 - (c + d)];
}
}
}
return count;
}
};
这道题给我一点点二分法的感觉,四个数组两两计算,复杂度可以一直控制在n^2,可见想做好这一类题还需要更多的刷题经验