代码随想录算法训练营第七天|Leetcode454 四数相加II、Leetcode383 赎金信、Leetcode15 三数之和、Leetcode18 四数之和
● Leetcode454 四数相加II
题目链接:Leetcode454 四数相加II
视频讲解:代码随想录|四数相加II
题目描述:给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:
0 <= i, j, k, l < n
nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
示例 1:
输入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2]
输出:2
解释:两个元组如下:
1.(0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0
2.(1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0
示例 2:
输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0]
输出:1
● 解题思路
方法一:暴力枚举
最直接的办法肯定就是暴力解决本题。我们只需要四层for循环去遍历每一种结果是否== 0
,并记录在结果count
中,将其返回即可。
但也正是用到了四个for循环,可想而知时间复杂度为O(n^4)
,时间复杂度将会非常高且必定超出时间限制。
时间复杂度: O(n^4)
空间复杂度:O(n^4)
方法二:哈希法
这道题为什么会使用哈希?我们知道哈希法的条件:查询某个元素是否在集合中出现或存在。
但我们需要在集合中存放什么元素,并用什么来在集合中find
呢?
根据题目中nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
,我们可以知道nums1[i] + nums2[j] = -(nums3[k] + nums4[l])
。因此,我们将nums1[i] + nums2[j]
存放在集合中,然后用-(nums3[k] + nums4[l])
去查找即可。
这样就可以将时间复杂度降为O(n^2)
时间复杂度: O(n^2)
空间复杂度:O(n^2)
● 注意事项
(1)为什么选择unordered_map
作为哈希结构?
我们在map中存放的不仅得包含nums1[i] + nums2[j]
的值,还需要存放该值出现的次数。
(2)unordered_map
结构中的key
和value
分别存放的是什么?
我们需要将nums1[i] + nums2[j]
作为key
存放在哈希结构中,在遍历nums3和nums4的时候需要根据key
返回出现的次数,即为value
。
(3)count为什么+= map[key]
?
map[key]
返回的是nums1 + nums2的和的次数,因为不同的nums1[i] + nums2[j]
的值可能相同,就可以和nums3[k] + nums4[l]
组成新的答案集,因此count应该+=map[key]
。
● 代码实现
方法二:哈希法
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int, int> map; //存放nums1[i] + nums2[j]的值
//获得所有nums1[i] + nums2[j]的值以及出现的次数
for(int i = 0; i < nums1.size(); i++)
{
for(int j = 0; j < nums2.size(); j++)
{
map[nums1[i] + nums2[j]]++;
}
}
int count = 0; // 记录返回结果
//在映射中查看所有-(nums3[k] + nums4[l])是否存在
for(int k = 0; k < nums3.size(); k++)
{
for(int l = 0; l < nums4.size(); l++)
{
int target = 0 - (nums3[k] + nums4[l]);
if(map.find(target) != map.end())
{
count += map[target];
}
}
}
return count;
}
};
● Leetcode383 赎金信
题目链接:Leetcode383 赎金信
题目描述:给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。
如果可以,返回 true ;否则返回 false 。
magazine 中的每个字符只能在 ransomNote 中使用一次。
示例 1:
输入:ransomNote = “a”, magazine = “b”
输出:false
示例 2:
输入:ransomNote = “aa”, magazine = “ab”
输出:false
示例 3:
输入:ransomNote = “aa”, magazine = “aab”
输出:true
● 解题思路
方法一:暴力枚举
最简单的办法就是使用两层for循环
去挨个遍历,外层for
遍历magazine
,内层遍历ransomNote
。当magazine[i] == ransomNote[j]
时,即有对应字符存在,同时还需要在ransomNote
中删除遍历过的字符,当其最后为空时,证明全部字符都有其对应。
时间复杂度: O(n^2)
空间复杂度:O(1)
方法二:哈希法
本题类似于有效的字母异位词,因此可以使用哈希法解决。
我们需要使用magazine
中的字符初始化哈希表,然后在用ransomNode
中每个字符去查询是否存在。因为本题有条件限制:
magazine 中的每个字符只能在 ransomNote 中使用一次
所以在每次遍历之后需要将hashtable中的值--
,否则就会出现同一个magazine
中的字符匹配ransomNode
中的字符。
时间复杂度: O(n)
空间复杂度:O(1)
● 代码实现
方法一:暴力枚举
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
for(int i = 0; i < magazine.length(); i++)
{
for(int j = 0; j < ransomNote.length(); j++)
{
if(ransomNote[j] == magazine[i])
{
ransomNote.erase(ransomNote.begin() + j);
break;
}
}
}
if(ransomNote.length() == 0)
{
return true;
}
return false;
}
};
方法二:哈希法
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int hash[26] = {0};
//magazine 中的每个字符只能在 ransomNote 中使用一次,所以需要判断以下情况
if (ransomNote.length() > magazine.length())
{
return false;
}
//将magazine的字符记录在hash中
for(int i = 0; i < magazine.length(); i++)
{
hash[magazine[i] - 'a']++;
}
//在哈希表中查询
for(int i = 0; i < ransomNote.length(); i++)
{
if(hash[ransomNote[i] - 'a'] != 0)
{
hash[ransomNote[i] - 'a']--;
}
else
{
return false;
}
}
return true;
}
};
● Leetcode15 三数之和
题目链接:Leetcode15 三数之和
视频讲解:代码随想录|三数之和
题目描述:给你一个整数数组 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 。
● 解题思路
方法一:哈希法
本题同样可以使用哈希法进行解决,我们需要遍历nums[i]
的同时对nums[i]
进行去重,再遍历nums[j]
并对其去重,然后在哈希结构中查询0 - (nums[i] + nums[j])
是否存在。但本题使用哈希法的话对于去重操作会相对于双指针法解决更加麻烦一点。
时间复杂度: O(n^2)
空间复杂度:O(n)
方法二:双指针
因为题目返回的是数组元素,因为可以对数组进行排序,打乱原来的下标顺序。
在排序的基础上,nums[i]
下标为0开始遍历,left
从i+1
,right从nums.size() - 1
开始判断三数之和是否为0。如果nums[i] + nums[left] + nums[right] > 0
,则需要将right--
缩小值以满足要求,但nums[i] + nums[left] + nums[right] < 0
时,则需要将left++
增大值满足要求。
时间复杂度: O(n^2)
空间复杂度:O(1)
● 注意事项
(1)nums[i]
什么条件下去重?
当我们的去重条件为nums[i] == nums[i + 1]
,我们去重的对象将是针对nums[i]
和nums[j]
,这种去重条件下将是对我们结果集中某一个组的三个元素进行去重。因此去重条件为nums[i] == nums[i - 1]
时,我们继续执行下一步操作。
(2)nums[left]
和nums[right]
什么时候去重?
当数组元素为{0,0,0,0,0}时,我们假如在放入{0,0,0}这一结果集之前对left
和right
进行去重的话,将会pass{0,0,0}进入结果集,导致返回结果不完全。因此需要我们在收录结果之后,再对left
和right
进行去重。
(3)while的边界问题?
假设while条件为left <= right
,此时当left == right
时,仍进入循环,但该组答案将会成为两个元素,不满足三元组条件。
● 代码实现
方法一:哈希法
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
for(int i = 0; i < nums.size(); i++)
{
if(nums[i] > 0) break; //nums[i]剪枝
if(i > 0 && nums[i] == nums[i - 1]) continue; //nums[i]去重
unordered_set<int> set;
for(int j = i + 1; j < nums.size(); j++)
{
//nums[j]去重
if(j > i + 2 && nums[j] == nums[j - 1] && nums[j] == nums[j - 2]) 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;
}
};
方法二:双指针
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
for(int i = 0; i < nums.size(); i++)
{
//当nums[i] == 0时,后面的元素都>= 0,相加不可能得到0
if(nums[i] > 0) return result;//nums[i]剪枝
//对nums[i]进行去重
if(i > 0 && nums[i] == nums[i - 1]) continue;
int left = i + 1, right = nums.size() - 1;
while(left < right)
{
if(nums[i] + nums[left] + nums[right] > 0)
{
right--;
}
else if(nums[i] + nums[left] + nums[right] < 0)
{
left++;
}
else
{
result.push_back({nums[i], nums[left], nums[right]});
//对nums[left]去重
while(left < right && nums[left] == nums[left + 1]) left++;
//对nums[right]去重
while(left < right && nums[right] == nums[right - 1]) right--;
left++;
right--;
}
}
}
return result;
}
};
● Leetcode18 四数之和
题目链接:Leetcode18 四数之和
视频讲解:代码随想录|四数之和
题目描述:给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [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
你可以按 任意顺序 返回答案 。
示例 1:
输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:
输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]
提示:
1 <= nums.length <= 200
-10^9 <= nums[i] <= 10^9
-10^9 <= target <= 10^9
● 解题思路
双指针:
四数之和的整体思路和三数之和的思路相同,但在三数之和的基础上在加入一层for
循环达到四个数的目的。不同于三数之和的剪枝操作if(nums[i] > 0)
,四数之和不能简单的判断if(nums[i] > target)
,假如其中一个结果集为{-4,-3,-2,-1},target为-10,将会导致pass该结果集的收录。因此对于其剪枝操作应该为nums[k] > target && nums[k] >= 0
。对于二级剪枝操作理论相同:nums[i] + nums[k] > target && nums[i] + nums[k] >= 0
。
同时还需要注意的是,题目中提示有:
-10^9 <= target <= 10^9;
因此nums[k] + nums[i] + nums[left] + nums[right]的计算结果可能大于10^9,需要将其强转为long。
时间复杂度: O(n^3)
空间复杂度:O(1)
● 代码实现
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
for(int k = 0; k < nums.size(); k++)
{
//一级剪枝
//nums[a]剪枝
if(nums[k] > target && nums[k] >= 0) break;
//nums[a]去重
if(k > 0 && nums[k] == nums[k - 1]) continue;
for(int i = k + 1; i < nums.size(); i++)
{
//二级剪枝
//nums[b]剪枝
if(nums[i] + nums[k] > target && nums[i] + nums[k] >= 0) break;
//nums[b]去重
if(i > k + 1 && nums[i] == nums[i - 1]) continue;
int left = i + 1, right = nums.size() - 1;
while(left < right)
{
if((long)nums[k] + nums[i] + nums[left] + nums[right] > target) right --;
else if((long)nums[k] + nums[i] + nums[left] + nums[right] < target) left ++;
else
{
result.push_back({nums[k], nums[i], nums[left], nums[right]});
while(left < right && nums[right] == nums[right - 1]) right --;
while(left < right && nums[left] == nums[left] + 1) left ++;
left ++;
right --;
}
}
}
}
return result;
}
};