代码随想录算法训练营第七天 | 哈希表理论基础 454.四数相加II 383. 赎金信 15. 三数之和 18. 四数之和

454.四数相加II

题目链接:454. 四数相加 II - 力扣(LeetCode)

视频链接:学透哈希表,map使用有技巧!LeetCode:454.四数相加II_哔哩哔哩_bilibili

文章链接:代码随想录

这道题我个人直观感觉是“1. 两数之和”的拓展,但四个数组搞得我不知道该构建哈希表来find什么(比如说我遍历到数组1的元素a,我不知道在下一个数组构成的hash表来find什么元素)。然后就去看参考视频了。

卡哥的视频解答了我的疑惑

既然我只会处理两个数组之和的情况,那么四个数组能不能先合并成两个数组来处理呢?

当然可以!我们可以让前两个数组相加划为一块,后两个元素相加划为一块。但也有可能会产生另一个问题。

这两块为什么要两两划分。我数组1为一块;数组2,3,4的和为另一块不行吗?

从算法时间复杂度来说,这样做不好。

因为当我们构建一块的过程中是要遍历相加的,这种方法的时间复杂度是O(n^{3}),而两两划分的时间复杂度是O(n^{2})。

进而衍生到如果出现“六数相加”、“八数相加”的题目,为了算法时间复杂度,划分是越平均越好。

在选用哈希数据结构的时候,也会陷入纠结。

结果不需要记录下标,那我是不是用set就能解决了呢?

其实并不对。

因为set会有去重的特性,所以当数组1的元素和数组2的元素相加的值跟set已有值重复的时候它是无法插入的。例如:[-1,-1],[1,1]这两数组之和出现了四个0。这四个0也算是四个组合,但set结构中没办法记录这个过程。所以我们可以采用map。其中,key为元素和,value为出现过的次数。上面那个例子就可以被记录成(0,4)键值对了。

个人代码如下:

class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        //构造遍历前两个数组之和a+b,key为值,value为出现的次数
        unordered_map<int,int> ab;
        //数组大小n
        int n = nums1.size(),ans = 0;
        for(int i = 0;i<n;i++){
            for(int j = 0;j<n;j++){
                auto it = ab.find(nums1[i]+nums2[j]);
                //如果ab里面key已有a+b,就把对应的value++
                if(it != ab.end()){
                    ab[nums1[i]+nums2[j]]++;
                }
                //反之,插入一个新元素到ab和,出现次数为1
                else{
                    ab.insert(make_pair(nums1[i]+nums2[j],1));
                }
            }
        }
        //遍历后两个数组的和c+d,看看在ab里面有没有-(c+d),如果有就ans加对应的val
        for(int i = 0; i < n;i++){
            for(int j = 0;j<n;j++){
                auto it = ab.find(-(nums3[i]+nums4[j]));
                if(it == ab.end()){
                    continue;
                }
                else{
                    ans += ab[-(nums3[i]+nums4[j])];
                }
            }
        }
        return ans;
    }
};

383. 赎金信

题目链接:383. 赎金信 - 力扣(LeetCode)

这道题跟“242.有效的字母异位词”很想,比较简单。

个人代码如下:

class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
        bool ans;
        //记录26个小写字母出现的次数
        int c[26] = {0};
        for(int i = 0;i<ransomNote.length();i++){
            c[ransomNote[i]-'a']--;
        }
        for(int i = 0;i<magazine.length();i++){
            c[magazine[i]-'a']++;
        }
        //遍历c数组,如果有元素小于0返回false,如果全部元素大于等于0就true
        for(int i = 0;i<26;i++){
            if(c[i]<0){
                return false;
            }
        }
        return true;
    }
};

15. 三数之和

题目链接:15. 三数之和 - 力扣(LeetCode)

视频链接:梦破碎的地方!| LeetCode:15.三数之和_哔哩哔哩_bilibili

文章链接:代码随想录

这道题暴力方法要用三重循环,算法时间复杂度O(n^{3})。在3 <= nums.length <= 3000的条件下,一定会超时,所以用不了。

这道题去重操作比较难理解,看完视频之后实现的个人代码如下:

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> ans;
        //对数组进行排序
        sort(nums.begin(),nums.end());
        //定义i,left和right指针
        int i = 0,left,right;
        //开始遍历i
        for(i;i<nums.size()-2;i++){
            //如果第一个元素大于零,就一定不存在这样的三元组
            if(nums[i]>0){
                return ans;
            }
            //如果nums[i]==nums[i-1],那么结果一定会重复,直接跳过即可。
            if(i!=0&&nums[i]==nums[i-1]){
                continue;
            }
            //i不重复就开始移动left和right
            else{
                left = i+1;
                right = nums.size()-1;
                while(left<right){
                    //如果nums[right] == nums[right+1],则必然会重复,故跳过
                    if(right!=nums.size()-1&&nums[right] == nums[right+1]){
                        right--;
                    }
                    else{
                        //可以开始考虑把答案放进去了
                        //如果结果大于0,就把right左移
                        if(nums[left]+nums[right]>-nums[i]){
                            right--;
                        }
                        //结果小于0,把left右移
                        else if(nums[left]+nums[right]<-nums[i]){
                            left++;
                        }
                        //结果等于0,把答案输入
                        else{
                            ans.push_back({nums[i],nums[left],nums[right]});
                            //因为right如果移动,nums[right]会变化,那么原来的left一定不满足条件,故left也得移动
                            left++;
                            right--;
                        }
                    }
                }
            }
        }
        return ans;
    }
};

卡哥给的参考代码如下:

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> 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<int>{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;
    }
};

双指针思路大体可以通过视频去理解,过程如图3.1所示,图像来源:代码随想录

图3.1双指针解题过程

关键的难点在于去重部分,怎么样才能把a,b,c进行去重操作?

首先,为了去重和双指针方便,我们需要对原数组进行排序预处理,这个预处理方法我估计在以后很多题目都会用上。

然后解释一下参考代码里面的注释

1.为什么a的去重可以直接通过判断之后右移指针实现,这样不会漏解吗?

不会漏解,可以看到图3.2。

图3.2

如果不去重,我们先判断i指向nums[0],找出集合S1里面和为-2的所有组合C1。然后i挪到nums[1]时,找出集合S2里面和为-2的所有组合C2。

我们易得:S1包含S2,所以C1一定包含C2!所以跳过nums[1]不会漏解。

2.为什么说a去重中判断nums[i] == nums[i + 1]是错误的方法?

因为我们是对a去重,如果这样判断就会把b==a的情况也忽略过去了,这不符合题干要求。我们要做的是不能有重复的三元组,但三元组内的元素是可以重复的!

3.如何对b,c去重

这里面有两种实现方法

  1. 先选中一个(a,b,c)三元组之后再对b,c去重(参考代码的方法)
  2. 选中三元组之前对b去重,c就不用去重了(我实现的代码)

我个人觉得参考代码更好理解一些。

该解法算法时间复杂度为O(n^{2})

18. 四数之和

题目链接:18. 四数之和 - 力扣(LeetCode)

文章链接:代码随想录

这道题目跟“三数之和”类似,多了个指针j,剪枝的思路也类似,所以就直接把代码写出来了。但有一个小细节要注意:题干里面说元素的范围是10^{-9} <= nums[i] <= 10^{9}”,又因为nums元素是int,如果直接四个元素相加很容易超出int表示范围2.1*10^{9},所以我们在相加的过程中得把其中一个元素强制类型转换成long,这样的话才不会溢出。

个人代码如下:

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        vector<vector<int>> ans;
        int i,j,left,right;
        //先对nums进行排序处理
        sort(nums.begin(),nums.end());
        //如果nums长度小于4或者前四个元素之和都大于target直接返回空数组
        if((nums.size()<4)){
            return ans;
        }
        else{
            //从第一个指针i开始处理
            for(i = 0;i<nums.size()-3;i++){
                //如果i前面有重复值就跳过
                if(i>0&&nums[i]==nums[i-1]){
                    continue;
                }
                //如果没有重复的话就开始处理第二个指针j,跟i的处理方式类似
                else{
                    for(j = i+1;j<nums.size()-2;j++){
                        if(j>i+1&&nums[j]==nums[j-1]){
                            continue;
                        }
                        //开始处理left和right
                        else{
                            left = j+1;
                            right = nums.size()-1;
                            while(left<right){
                            //可能溢出,转成long
                                if(long(nums[i])+nums[j]>target-long(nums[left])-nums[right]){
                                    right--;
                                }
                                else if(long(nums[i]+nums[j])<target-nums[left]-nums[right]){
                                    left++;
                                }
                                else{
                                    ans.push_back({nums[i],nums[j],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 ans;
    }
};

算法时间复杂度是O(n^{3})。我总结一下双指针法:

双指针法其实是暴力法的一种剪枝,将时间复杂度降一个数量级。例如:O(n^2)的解法优化为 O(n)的解法。

等之后刷完字符串再尝试对双指针法进行总结。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值