详解 LeetCode 454.四数相加||、赎金信、三数之和、四数之和 || 代码随想录 day 6

一.四数相加

1.题目描述

给你四个整数数组 nums1nums2nums3 和 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

2.思路分析

本题乍一看和之前的两数之和相似,但两数之和利用哈希法好求解,此题再利用哈希就不好解了。因为哈希法在不超时的情况下还要去重是很难的,(虽然网站提供了相应解法,但对于我这种小白来说,一刷还是乖乖按大佬的步子来吧)

所以,本题采用普通循环来做:

1)定义 unordered_map 用来存放 A 和 B 数组中两两数结合所求得的 和值 以及这个和值 出现的次数。

2)遍历 C 、D 数组,找是否有 (0 - c - d)的值出现在 上述的map容器中,若有,则说明 0 - c - d = a + b,将c 、d 移过来 ,就得到了 a + b + c + d = 0,此时,只要记录了 事先存好的 a+b 出现的次数就好了。(因为本题不要求去重,所以 a+b 出现的次数就是最终符合条件的元组数)

附代码:

class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        unordered_map <int, int> ret;
        int count = 0;
        for(int a : nums1) {
            for(int b : nums2) {
                ret[a+b]++;
            }
        }  //遍历并记录所有 a+b 的值
        for(int c : nums3) {
            for(int d : nums4) {
                int target = 0 - c - d;
                if(ret.find(target) != ret.end()) {
                    count += ret[target];
                }
            }
        } return count;

    }
};

注意:

(1)为什么是 A + B 两个两个相加和 C + D 两个两个相加的和作比较而不是 A 和B+C+D这样的比较方式呢?

因为两个两个相加,其就是一个大循环套一个小循环,时间复杂度就是 n^2,两个比较,总程序的时间复杂度就是 2n^2 ------> n^2;如果是一个三个的计算,其总时间复杂度就是 n+n^3 ------> n^3故从时间复杂度来看,两个两个更优。

(2)count 每次加的值要注意不是1.

因为题目不要求去重,所以 a+b 的值和 c+d 匹配了,就是一个符合条件的元组,那么 map 中存放了几个这样的 a+b ,就有几个符合条件的元组,所以,count 每次增加的值就是 符合条件的 在map中存放着的个数。

注意: 定义 map 的时候不要忘记给初值 0 !!!

(3)在 C++中,用于遍历数组和容器的简易写法:

for(类型  变量名 : 数组或容器)

只适用于C++且只适用于数组 / 容器!!! 

二.赎金信

1.题目描述

给你两个字符串: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

力扣题目链接

2.思路分析

本题和字母异位词极为相近!大致代码也相同

不过要注意以下几点:

(1)字母异位词不管是谁放入容器中进行字母数字统计判断都可以,但本题只能是 maganize 放入容器中。

(2)添加了新的前置条件:当 ransomNote 的长度大于 magazine 时,2 肯定组不成 1 了,直接返回即可。

附代码:

class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
        int result[26] = {0};
        if(ransomNote.size() > magazine.size())
            return false;
        for(int i = 0; i < magazine.size(); i++) {
             result[magazine[i] - 'a']++;
        }
        for(int i = 0; i < ransomNote.size(); i++) {
            result[ransomNote[i] - 'a']--;
        }
        for(int i = 0; i < 26; i++) {
            if(result[i] < 0)   //小于0,说明ma中字母不足够ran使用
                return false;
        }
        return true;
    }
};

三.三数之和

1.题目描述

力扣题目链接(opens new window)

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。

注意: 答案中不可以包含重复的三元组

示例:

给定数组 nums = [-1, 0, 1, 2, -1, -4],

满足要求的三元组集合为: [ [-1, 0, 1], [-1, -1, 2] ]

2.思路分析

本题再使用哈希法就不合适了,因为哈希法去重真的很困难!(因为哈希找某一数值时是无序查找的,所以难免会有重复的时侯)

所以本题采用双指针的求法:

(1)大致思路是,先确定一个数值 a,然后利用两个指针去寻找 符合条件的 b 、c 的值。

(2)不管是 a 还是找 b 、c,如果这个集合是无序的,那么在找到一组 b、c 时,如果三者相加和 大于 0 或者小于 0 ,指针是无法进行我们想要的移动的:例如让 b 小一点或让 c 大一点。所以为了方便,我们要先对集合进行排序。

(3)排序我们采用的是 从小到大 的次序,而不用 从大到小 的次序。例如数组全是正数,我们本可以利用 小到大的 次序在初始时判断首值 >0, 就说明这个数组无论再怎么相加也不会得到 0 ,这种情况就直接返回退出了,不必再跑多余的循环;而利用 大到小 的次序,就必须全部跑完一遍才行。

(4)题目要求的是 不能重复的三元组,但是每个元组内的 元素 是可以重复的。例如 元组            { -1,-1,2} ,不能因为其内有两个相同的 -1 就说他不符合条件。但是不能出现 {-1,-1,2 } 和

{ -1,-1,2}的情况(因为有的时候,第一个 -1 在数组中取值位置并不同,例如第一个元组中 a 的位置在数组中的第 0 位,而第二个元组中 a 的位置在数组中的 第 1 位,不能因为这个就说他俩是不同的)

(5)本题重点:去重

分为两种:

1)对 a 进行去重:

假设已排好的数组是: -4  -1  -1  0  1  2

大致思路是:a 依次取到这个数组的每一个值,b 是 a 的下一位,c 是从数组最后开始往前遍历。

例如,a 取 -4 时,b、c 将其余数值全部遍历完后未找到 相应的能使 a+b+c = 0 的值,所以 a 依次取到下一个值 -1。 当 a = -1 时,b = -1,c = 2,此时刚好符合条件,将这组数据存放到容器中,之后,a 不变,b 、 c 继续遍历,看剩余数值中是否还有符合条件的值。则 b = 0,c = 1,也符合条件,存放。此时已没有剩余数据,故退出此次循环,进入下一个循环中。

但 由于 a 的下一个取值 是 -1,a 作为 -1 的全部能符合条件的 b 、 c 已经遍历过且保存过了,此时如果再进行一次访问,那么最后结果势必会重复。所以,a 应该跳过此次循环,进入在下一个循环。

那么此思路如何提现到代码中呢?

我们直到,去重的本质就是找相同值,在数值中,无非就是 num[ i ]  == num[ i+1 ]  或者 num[ i ] == num[ i - 1 ] 罢了。那么究竟该写哪种呢?

还是根据上面的思路来找。

如果是第一种,那么 a 在取第一个 -1 时,由于进行了 此值和下一个值 的判断,二者相等,便会跳过 这个值,就是说,最后的结果中 ,连第一个 取 -1 符合条件的值都没了。也就是没了{-1,-1,2}这个元组,那么结果自然是不对的。

那么就是第二种了。第二种写法的思路是,当 a 取到第二个 -1 时,由于他会进行此值与前一个值的比较判断,发现二者值相等,也就是说,不用再进行此次循环了,直接跳过。这样既保留了第一个 -1 ,又去重了 第二个 -1.

2)对 b、c 的去重

若集合是这样: 0  -1  -1   -1   -1  1  1  1   1  

在 a = 0 时,b = -1,c = 1,这个元组就是符合条件的,而之后的 b、c 值都相同,是不需要再遍历的,所以此时要对他们去重。

这里要注意两点:

1.条件要使用 while 而不是 if,像上述集合的情况,如果使用的是 if,那么只会去重一次,后面的重复数值还是没被去掉,还是会得到重复答案

2.去重代码的位置

不能一进入大循环找 b、c 的开始就去重,这样的话,连第一个 {0,-1,1}都不会得到了,因为第一个 -1 和 1 都被去重掉了。

附代码:

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> ret;  //存放最后结果的容器
        sort(nums.begin(), nums.end());  //对数组进行排序:sort默认从小到大

        for(int i = 0; i < nums.size(); i++) {
            if(nums[i] > 0) {   //判断首元素是否大于0,是则直接退出
                return ret;
            }
            if(i > 0 && nums[i] == nums[i-1]) 
                continue;    //对a进行去重
            
            int left = i + 1;
            int right = nums.size()-1;

            while(left < right) {
                if(nums[i] + nums[left] + nums[right] > 0 ) right--;   //大于0,c调小一点
                else if(nums[i] + nums[left] + nums[right] < 0 ) left++;
                else {
                        ret.push_back(vector<int>{nums[i], nums[left], nums[right]});
                        while(left < right && nums[left] == nums[left+1]) left++;
                        while(left < right && nums[right] == nums[right-1]) right--;
                        left++;
                        right--;
                }
            }
        }
        return ret;

    }
};

 注意:

(1)对 a 去重时,因为用的有 num[ i -1] , 所以我们要保证 i > 0 ,否则 i - 1 可能 < 0,导致越界访问出错。

(2)while 循环条件为什么是 left < right 而不是 left <= right。

因为如果相等,两个指针指向同一个数,题目要找的是三元数组,这样只能找到两个元素,自然就不对了。

(3)在 while 循环里,有 三个 if 语句,不能写成 : if ,if , else if 。因为本质上我们想让他访问完第一个 if 后,在访问第二个 if ,然后在访问第三个 if ,而这样写,因为 if 和 else 是成对出现的,在访问完第一个 if 后,他就直接去找与他配对的 第三个 if 前的 else 而直接跳过 第二个 if 了。

(这个错误真的很细很难发现!!!(对我来说)

四.四数之和

1.题目描述

给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):

  • 0 <= a, b, c, d < n
  • abc 和 d 互不相同
  • nums[a] + nums[b] + nums[c] + nums[d] == target

你可以按 任意顺序 返回答案 。

https://leetcode.cn/problems/4sum/

2.思路分析

本题和三数之和的解题思路大致相同,具体有以下几点不同:

(1)剪枝不同

三数之和的剪枝,也就是对 首元素 起始的判断,是利用 : num[ i ] > 0 剪枝的,本题不能这么剪枝,原因是,target 值不确定。如果 target 值为 -5,集合是 -4,-1,0 时,首元素大于 target 就直接剪枝了,不出意外的话就出意外了。

本题的剪枝是: num[ k ] > 0 & target > 0 & num[ k ] > target

(2)与三数不同的是,还要有一个外循环 ,k 代表 第一个数,a ,b,c 分别代表剩余三个数,在求解过程中,为了方便求 b,c 的值,可以将 k+a 作为一个整体,就类似于上题中 的 a 值。

剩余注意事项和上题大差不差。

(3)判断的时候,注意四值相加可能会溢出,要强制转换成 long 型

附代码: 

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        vector<vector<int>> ret;
        sort(nums.begin(), nums.end());
        for(int k = 0; k < nums.size(); k++) {
            if(nums[k] >= 0 && nums[k] > target) {   //一级剪枝
                break;
            }
            if(k > 0 && nums[k] == nums[k-1]) {   //一级去重: 对k去重
                continue;
            }

            for(int i = k + 1; i < nums.size(); i++) {
                if(nums[k] + nums[i] >= 0 && nums[k] + nums[i] >target) {   //将 k+a 作为一个整体,二级剪枝
                    break;
                }
                if(i > k + 1 && nums[i] == nums[i-1]) {   //对a去重
                    continue;
                }
                int left = i + 1;
                int 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 {
                        ret.push_back(vector<int>{nums[k], nums[i], nums[left], nums[right]});
                        while(left < right && nums[left] == nums[left+1]) left++;
                        while(left < right && nums[right] == nums[right-1]) right--;

                        left++;
                        right--;
                    }
                }
            }
        }
        return ret;
    }
};

今日学习真的学的我头昏眼花,前两个题还好,三数相加和四数相加真的是!梦破碎的地方!细节真的太多了太杂了!稍微一不注意就会犯错,而且不容易注意到的地方也很多!

但是弄清楚又会有莫大的快乐,今天是梦不断破碎而我又不断捡起来拼好的一天。

我需要学习的地方还很多,继续加油吧!

(终于结束今天的学习了啊啊啊、啊啊啊、啊啊啊、已发疯...)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值