LeetCode454.四数相加
给你四个整数数组
nums1
、nums2
、nums3
和nums4
,数组长度都是n
,请你计算有多少个元组(i, j, k, l)
能满足:
0 <= i, j, k, l < n
nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
思路: 这道题和下面的三数之和、四数之和很像,但是,也只是像而已,实际上比它们要简单得多。(不信可以看看下面的三数之和、四数之和讲解)
这里依然是使用哈希法。使用unordered_map来存储,其中键为第一个和第二个数组的和的所有情况,而键值则是对于每种和的情况所出现的次数。当我们得到和的情况后,两层循环嵌套,找出再另外两个数组中是否存在满足情况的 0 -(nums[i] + nums[j]),如果map中有键值和其匹配,则将其键值加入统计变量count中。最后统计完成输出即可。
时间复杂度:O(n^2);
空间复杂度:O(n^2);
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int, int> map; //创建map
int count = 0; //统计满足题意的情况有多少种
for(int a: nums1){
for(int b: nums2){
map[a+b] ++; //统计nums1和nums2中的元素和的出现情况
}
}
for(int c: nums3){
for(int d: nums4){
if(map.find(0 - (c + d)) != map.end()){ //如果满足情况,使count加上和出现的次数
count += map[0 - (c + d)];
}
}
}
return count;
}
这里空间复杂度为什么是O(n^2),因为首先统计的两个数组可能元素各不相同,导致和的最坏情况有n^2种。
虽然时间复杂度看起来仍然很大,O(n^2),但是想一想,如果暴力求解呢,那恐怕就是O(n^4)了,所以这样看起来,时间复杂度也小了不少。
LeetCode383赎金信
给你两个字符串:
ransomNote
和magazine
,判断ransomNote
能不能由magazine
里面的字符构成。如果可以,返回
true
;否则返回false
。
magazine
中的每个字符只能在ransomNote
中使用一次。给你两个字符串:
ransomNote
和magazine
,判断ransomNote
能不能由magazine
里面的字符构成。如果可以,返回
true
;否则返回false
。
magazine
中的每个字符只能在ransomNote
中使用一次。
思路:这道题其实和之前做过的有效的字母异位词有着同样的解法。使用数组作为辅助数组,记录下出现的小写字母情况,然后再看另一个对象中的元素是否满足情况,不同的是,有效的字母异位词是需要互相验证,而这里对于magazine来说,是只需要验证ransom是否能够由magazine中的字符组成,不用验证magazine是否能由ransom字符构成,所以相对来说更加简单,限制条件也很容易判断出来。
时间复杂度:O(n);
空间复杂度:O(1);
bool canConstruct(string ransomNote, string magazine) {
int record[26] = {0}; //设置辅助数组
if(ransomNote.size() > magazine.size()) return false;//当ransomNote的字符数大于magazine时,可以知道magazine肯定没有办法构成sansomNote
for(int i = 0; i < magazine.size(); i++){
record[magazine[i] - 'a'] ++; //统计magazine中的字符出现情况
}
for(int i = 0; i < ransomNote.size(); i++){
record[ransomNote[i] - 'a'] --;
if(record[ransomNote[i] - 'a'] < 0){
return false;//当辅助数组中出现了小于零的数,说明ransomNote中包含了其他字符,没办法由magazine构成
}
}
return true;
}
LeetCode15.三数之和
给你一个整数数组
nums
,判断是否存在三元组[nums[i], nums[j], nums[k]]
满足i != j
、i != k
且j != k
,同时还满足nums[i] + nums[j] + nums[k] == 0
。请你返回所有和为0
且不重复的三元组。注意:答案中不可以包含重复的三元组。
思路:这里就来到了三数之和,之前说过,和四数相加很像,但是更加复杂。 因为要求返回答案中不可以包换重复的三元组。这是很棘手的一个问题。
这里提供两个方法:
1、哈希法
首先我们需要一个result数组,用于之后存储返回元素。对数组先排序,使其有序,然后两层循环嵌套使用,不断剪枝,不断去重,中间需要注意一些细节,最后进行元素判断,检查其是否存在与set集合中,避免重复,配对成功还需要及时删除。当循环完成后返回result。、
时间复杂度:O(n^2)
空间复杂度:O(n)
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[0] > 0) break; //当第一个数为正数时,后面的数必然为正,没有办法满足题意
if(i > 0 && nums[i] == nums[i - 1]){ //三元组第一个元素去重
continue;
}
unordered_set<int> st;
for(int j = i + 1; j < nums.size(); j ++){
if(j > i + 2 && nums[j] == nums[j - 1] && nums[j - 1] == nums [j - 2]){
continue;
} //三元组第二个元素去重,为什么会是到j-2,可以以[0,0,0,0]这个测试用例来分析,当前三个组成题解后,第四个数按理来说不能再参与其中,所以会有上面的式子。当然这只是帮助理解
int c = 0 - (nums[i] + nums[j]);
if(st.find(c) != st.end()){
result.push_back({nums[i], nums[j], c});
st.erase(c); //第三个元素去重
}else{
st.insert(nums[j]);
}
}
}
return result;
}
其实相对于暴力求解来说,这种方法的时间复杂度确实降低了不少,虽然力扣上能过,但是其稍微有些繁琐,而且所用时间消耗还是挺大,一些剪枝细节以及去重细节需要格外注意,否则极容易出bug。 所以这里推荐第二种方法。
2、双指针法
之前提到过很多次双指针方法解决问题,比如在环形链表II、移除元素等等,可以说很常见。这里我们依然使用双指针来解这道题。
首先我们还是需要对原数组进行排序,这也是这道题使用双指针法的关键。然后进入第一层循环,进行剪枝操作、去重操作,然后将left指针指向i+1节点,righ指向最后,于是进入了while循环中,对i、left、right的索引目标值求和,因为本身数组已经是有序的了,如果和大了,则right小一点,开始right--;如果和小了,left大一点,left++;最后如果成果匹配,则将匹配对按照对应格式加入result中,然后开始分别对left、right所指向的元素去重,注意后面还需要right--,left++一下,避免出现重复的结果。循环跳出后,最后返回结果即可。
时间复杂度:O(n^2)
空间复杂度:O(1)
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
int left, right; //设置双指针
for(int i = 0; i < nums.size(); i ++){
if(nums[i] > 0){
return result;
}
if(i > 0 && nums[i] == nums[i - 1]){
continue;
}
left = i + 1;
right = nums.size() - 1;
while(right > left){
if(nums[i] + nums[left] + nums[right] > 0) {
right --;//说明三元素之和太大,将最大的right指针往后移
}else if(nums[i] + nums[left] + nums[right] < 0){
left ++;//说明三元素之和太小,移动left指针
}else{
result.push_back(vector<int>{nums[i], nums[left], nums[right]});
while(right > left && nums[right] == nums[right - 1]) right --;
while(right > left && nums[left] == nums[left + 1]) left ++;//去除相同元素
right --;
left ++;
//上面的循环中的left和right指向的元素已经添加过了,
//所以还需要再移动一次,避免重复元素
}
}
}
return result;
}
可以看到双指针法相对于哈希法来说,确实要高效简洁一点。
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
你可以按 任意顺序 返回答案 。
思路:当你彻底弄明白三数之和后,四数之和也变得简单了,同样是双指针法,不过多层循环嵌套罢了。不过还是得注意一下剪枝细节以及去重细节。前面是nums[i]固定,left和right指针在while循环里面移动;这里是nums[i] + nums[j]固定,同样是left和right指针在while循环里面移动。
这里还有一个细节,就是求nums[i]+nums[j]+nums[left]+nums[right]的时候前面得加上(long)类型符,因为这些数之和可能会超限,无法通过。
时间复杂度:O(n^3)
空间复杂度:O(1)
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> result;
sort(nums.begin(), nums.end()); //对数组进行排序
int left, right; //设置双指针
for(int i = 0; i < nums.size(); i ++){
if(nums[i] > target && nums[i] >= 0) break;
//当第一个元素都大于target时,后面的元素再相加必然会大于target
if(i > 0 && nums[i] == nums[i - 1]) continue;//第一个元素去重
for(int j = i + 1; j < nums.size(); j ++){
if(nums[i] + nums[j] > target && nums[i] + nums[j] >=0) break;
//这里可以写成nums[i]+nums[j]>target&&nums[j]>=0;因为当有nums[j]>=0时,
//说明后面必然是正数,再加上表达式前面的条件,可以知道其和肯定不为target
if(j > i + 1 && nums[j] == nums[j - 1]) continue;
left = j + 1;
right = nums.size() - 1; //指定双指针位置
while(right > left){
if((long)nums[i] + nums[j] + nums[left] + nums[right] > target) right --;
else if((long)nums[i] + nums[j] + nums[left] + nums[right] < target) left ++;
//nums[i]+nums[j]+nums[left]+nums[right]会超限,得加上long
else{
result.push_back({nums[i], nums[j], nums[left], nums[right]});
//找到了对应元素,将其添加进result
while(right > left && nums[right] == nums[right - 1]) right --;
while(right > left && nums[left] == nums[left + 1]) left ++; //元素去重
right --;
left ++; //避免出现重复元素
}
}
}
}
return result;
}
其实当把三数之和与四数之和搞清楚后,五数之和、六数之和等也就明了了,多个数就多层循环,不过得注意求和的时候是否超限。双指针法将这些题目的复杂度相对于暴力求解来说都降低了一个数量级,比如三数之和暴力求解时间复杂度为O(n^3),双指针法求解时间复杂度为O(n^2);四数之和暴力求解时间复杂度为O(n^4),双指针法求解时间复杂度为O(n^3)等等。
所以学好双指针法还是很有用的。
希望我的文章能够帮助到你,如果有帮助,麻烦点赞加收藏,再点点关注,非常感谢!