概述
- 这三题可一起分析这种【多数之和】问题
- 四数之和的另一种形式
分析
1
-
两数之和比较简单,暴力就直接两种循环就可以了,复杂度是 O ( N 2 ) O(N^2) O(N2)
-
实际上,第二重循环是寻找特定的元素,所以我们可以用一个map,利用key值来记录数组中包含的所有元素,进而可以直接替代循环直接找到所需的元素
-
当然,我们还可以先将数组排序,然后利用双指针,分别从头、尾开始移动,可以直接确定两个元素;
但是,排序的复杂度 O ( N l o g N ) O(NlogN) O(NlogN),所以这里不使用
15
- 三数之和仍然可以使用暴力,三重循环,但是到了三重循环基本上都会超时,所以我们需要观察题目要求简化
- 首先,示例1告诉我们,存在多个解,并且输入数组中是可以有重复的元素的,所以我们不能想第1题那样使用map来记录
- 所以,我们考虑上面提到的排序+双指针,这样可以减少两重循环,使得复杂度变成第一种循环+双指针循环 O ( N l o g N + N 2 ) O(NlogN+N^2) O(NlogN+N2)
18
-
首先,肯定不能暴力解决了,这题实际上方法和第15题一样,也是排序+双指针法
可能有人会怀疑,即使这样复杂度也是 O ( N 3 ) O(N^3) O(N3),会不会超时
-
这个时候,我们就需要看一些数据,题目限制了数组长度是200,所以即使是三次方也就 1 0 6 10^6 106,不会超时
-
但是注意时,这题四数的和不一定是0
代码中有所体现
454
-
首先,这题同样是一个四数相加问题,但是这里的输出结果与上面的不同,需要我们输出索引,因为我们不能对输入的数组做任何排序动作,也就无法使用双指针
-
所以,我们这题只能使用之前的map数组映射
-
如果只对一个数组二分,最终复杂度是 O ( N 3 ) O(N^3) O(N3),在此题下可以接受;
但是我们可有选择将数组的和保存,这样复杂度是 O ( N 2 ) O(N^2) O(N2)
具体见代码注释
思路
1
如何利用map呢?
-
首先key一定等于数组中的值,这样才能利用key直接确定数组中是否有该值
-
map中的value是否不需要利用呢? 并不是。因为题目要求同一个元素不能重复出现,所以value应该保留key值对应的数组的下标,通过下标确定该元素不是第一重循环此时固定的元素
这里的前提是因为题目说明只会有一个答案,说明数组中没有重复的值,才可以这样操作;
如果考虑有两个3,然后我们的目标是6,实际上两个3之和就等于6,但是我们使用map就只能记录一个map,无法获得解答
15
双指针如何确定两个元素并且不重复呢?
-
利用双指针指向排好序的数组,初始时
left
指向数组最小值,right
指向数组最大值,我们记目标值为target
- 如果
nums[left] + num[right] > target
,则需要减少,则right
向左移 - 相反,则
left
向右移
- 如果
-
那么如何确保不重复呢?
-
首先,因为我们是排好序的数组,所以每次第一轮循环确定了一个元素,然后我们双指针指向的是该元素之后的数组
-
其次,因为可能存在有多个解,所以我们在双指针时获得一组解后,不能直接退出,应该继续双指针向后,寻找可能不同的第二组解;
但是,因为有重复的元素,所以可能会找到重复的解
又因为我们是排序过的,所以重复的值一定在一起,所以我们寻找第二组不同的解是,应该跳过上一组解重复的值(代码中注释)
-
代码
1
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int,int> unordered_map_int_to_bool;
for (int i = 0; i < nums.size(); ++i)
unordered_map_int_to_bool[nums[i]] = i; // 记录num[i]对应的数组中的索引
for (int i = 0; i < nums.size(); ++i)
if (int j = unordered_map_int_to_bool[target - nums[i]]) // 首先判断该元素是否存在
if (i != j) // 接着判断该元素是否和此轮循环的元素重复
return {i,j};
return {};
}
};
15
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(), nums.end()); // 排序
vector<vector<int>> result;
for (int i = 0; i < nums.size(); ++i) {
if (nums[i] > 0) { // 剪枝,因为和是固定0,所以可以这样操作
return result;
}
if (i != 0 && nums[i] == nums[i - 1]) // 跳过重复的值再开始
continue;
// 双指针,注意left=i+1
int left = i + 1, right = nums.size() - 1;
int target = 0 - nums[i];
while (left < right) {
if (target == nums[left] + nums[right]) {
result.push_back({nums[i], nums[left], nums[right]});
// 跳过重复的值
++left; // 这里left先++,是为了确保left一定会向前移动,即使没有进入while(也有其他的方法)
while(left < nums.size() &&nums[left] == nums[left - 1]) ++left; // 跳过和该组解重复的值
--right;
while( right > i + 1 && nums[right] == nums[right + 1]) --right;
}else if (target > nums[left] + nums[right])
++left;
else
--right;
}
}
return result;
}
};
18
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
sort(nums.begin(), nums.end());
vector<vector<int>> result;
for (int i = 0; i < nums.size(); ++i) {
// 这里没有剪枝操作了,因为可能和是负数,-4+-2 = -6,但是-4 > -6
if (i != 0 && nums[i] == nums[i - 1])
continue;
for (int j = i + 1; j < nums.size(); ++j) { // 两重循环
if (j != i + 1 && nums[j] == nums[j - 1])
continue;
int left = j + 1, right = nums.size() - 1; // left从j+1开始
int target_temp = target - nums[i] - nums[j];
// 双指针代码和15
while (left < right) {
if (target_temp == nums[left] + nums[right]) {
result.push_back({nums[i], nums[j], nums[left], nums[right]});
++left;
while(left < nums.size() && nums[left] == nums[left - 1]) ++left;
--right;
while( right > j + 1 && nums[right] == nums[right + 1]) --right;
}else if (target_temp > nums[left] + nums[right])
++left;
else
--right;
}
}
}
return result;
}
};
454
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int,int> unordered_map_int_to_int_1;
for (auto n1 : nums1)
for (auto n2 : nums2)
++unordered_map_int_to_int_1[n1 + n2]; // 会有重复的和,所有需要利用value来保存有几种
int result = 0;
for (auto n3 : nums3)
for (auto n4 : nums4)
result += unordered_map_int_to_int_1 [0 - (n3 + n4)];
return result;
}
};