代码随想录算法训练营第七天|454.四数相加、383.赎金信、15.三数之和、18.四数之和

四数相加

        我的思路是创建1个哈希表numMap1存储nums1、nums2各元素相加结果、numMap2存储nums3、nums4中各元素相加结果,由于最后只需考虑返回组合的数量而不是实际的组合列表,我用哈希表记录相加结果的次数,如假设nums1[1,2]、nums2[2,3],则哈希表numsMap1[3]=1、numsMap[4]=2、numsMap[5] = 1,同理在numsMap2中也会存储类似结构,这时,为了实现四数相加为0的结果,需要查找numsMap1和numsMap2关键字相反的数据,如numsMap1中有关键字3、4、5,我们若想四数相加为0,则需要numsMap2中寻找相反数-3、-4、-5,假设找到numsMap2[-4]=2,则最终结果count需要加上2*2,即在numsMap1中相加和为4的有2对,在numsMap2中相加和为-2的有2对,组合起来,共有4对四数相加为0。以此类推,遍历完一个哈希表就能得到四数相加为0的所有可能对数。

class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        int n = nums1.size();
        std::unordered_map<int,int>numMap1;
        std::unordered_map<int,int>numMap2;//创建2个哈希表来分别存储第一二数组、第三四数组中元        
                                          //素相加的结果
        for(int i = 0;i<n;i++){
            for(int j = 0;j<n;j++){
                numMap1[nums1[i]+nums2[j]] += 1;//相加结果每相同一次,哈希表中值加1
                numMap2[nums3[i]+nums4[j]] += 1;
            }
        }
        int count = 0;//创建返回值count
        for (auto&x:numMap1){
            if(numMap2.count(-x.first)){//遍历哈希表1时,若发现哈希表2中存在有值与哈希表1相反
                                        //count+=两数相乘
                count += (numMap1[x.first]) * (numMap2[-x.first]);
            }
        }
        return count;//返回count
    }
};

算法的空间复杂度为O(n),时间复杂度为O(n^2)。

上述自己想的方法用了两个map导致算法的内存消耗太大。贴一下代码随想录的结果,旨在使用一次map,在第二次循环中直接查找结果,更快。

class Solution {
public:
    int fourSumCount(vector<int>& A, vector<int>& B, vector<int>& C, vector<int>& D) {
        unordered_map<int, int> umap; //key:a+b的数值,value:a+b数值出现的次数
        // 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中
        for (int a : A) {
            for (int b : B) {
                umap[a + b]++;
            }
        }
        int count = 0; // 统计a+b+c+d = 0 出现的次数
        // 在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就把map中key对应的value也就是出现次数统计出来。
        for (int c : C) {
            for (int d : D) {
                if (umap.find(0 - (c + d)) != umap.end()) {
                    count += umap[0 - (c + d)];
                }
            }
        }
        return count;
    }
};

赎金信

        与有效的字母异位词类似,创建一个26位的数组,第一次遍历magazine,将magazine中的元素数目记录在数组中,在第二次遍历ransomNote时,每遍历一次,数组对应索引保存元素-1,当存在一个索引保存元素小于0时,说明magazine中的字母无法表示ransomNote,返回false,否则返回true。

class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
        vector<int> ivect(26, 0);//创建26个字母的数组,代表‘a’~‘z’
        for (int i = 0; i < magazine.size(); i++) {
            ivect[magazine[i] - 'a']++;
        }//遍历magazine,将magazine中元素对应数组的索引保存元素自加1
        for (int j = 0; j < ransomNote.size(); j++) {
            ivect[ransomNote[j] - 'a']--;遍历ransomNote并将元素对应数组的索引保存元素自减1
            if (ivect[ransomNote[j] - 'a'] < 0) {//若存在元素小于0,则说明无法构建
                return false;
            }
        }
        return true;//否则可以构建,返回true
    }
};

三数之和

哈希表

        有考虑使用哈希表来解决,遍历两遍数组,将数组所有两次相加的可能结果(nums[i]+nums[j],{nums[i],nums[j]})存入一个哈希表,映射方式为键可重复的multimap,multimap<int.vector<int>>numsmap;之后再遍历一遍数组,寻找numsmap中与数组元素相反的键及其值,这里就出现了去重的问题,去重有2个,一是数组中的重,如数组如果全为0,则只需考虑一种结果,二是numsmap中的重,如考虑三个键值对{0:[-1,1]}{0:[-2,2]}和{0:[-1,1]},只需考虑{0:[-1,1]}和{0:[-2,2]}。未成功做出,贴出代码随想录的哈希表做法。

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[j], c = -(a + b)
        for (int i = 0; i < nums.size(); i++) {
            // 排序之后如果第一个元素已经大于零,那么不可能凑成三元组
            if (nums[i] > 0) {
                break;
            }
            if (i > 0 && nums[i] == nums[i - 1]) { //三元组元素a去重
                continue;
            }
            unordered_set<int> set;
            for (int j = i + 1; j < nums.size(); j++) {
                if (j > i + 2
                        && nums[j] == nums[j-1]
                        && nums[j-1] == nums[j-2]) { // 三元组元素b去重
                    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;
    }
};

算法的时间复杂度O(n^2),空间复杂度O(n)。

双指针法

        已知数组nums,需找nums中的非重合元素[a,b,c]使得a + b + c = 0; 

        需先对数组nums进行排序,使nums有序。以for循环遍历数组,其中指示变量i以及我们创建的left = i + 1及right指针用于返回符合要求的结果。具体流程参考算法随想录哈希表下三数之和。

        基本流程如下,创建结果二维数组vector<vector<int>> result,在对数组nums排序后,进行循环,若第一个元素已大于0,则直接返回结果二维数组result(排序后若第一个元素大于0,之后无论如何加都不会得到0),令left = i+1;right = nums.size()-1;当(nums[i]+nums[left]+nums[right]>0)时,令right--,这里主要是由于数组已排序,left不能向前,只能减小right,当(nums[i]+nums[left]+nums[right]<0)时,令left++,道理同上,right不能后移,只能++left;此外就是相等的情况,将vector<int>{nums[i],nums[left],nums[right]}存入result数组,此时,需要同时改变left和right数组,left++,right--。若只是这样遍历,会存在很多重复的结果,考虑题意要求的唯一答案。

        考虑这样,需要对i、left和right所指向数据进行去重,第一次是对i的去重,若(i>0 and nums[i] == nums[i--])令i++,同时continue,这个在i = 0时是不会执行的,这里的条件其实主要有两种可能,分别为num[i] == num[i--]和nums[i] == nums[i++],但第二个条件会遗漏[-1,-1,2]这样的情况(因为right = i+1),所以选择(i>0 and nums[i] == nums[i--]),这次去重的位置选在判断完nums[0]>0后,针对right和left的去重选择在一次输出结果之后,如[0,-1,-1,-1,-1,1,1,1,1]这样的数组,需要去重,在对left和right更新前,将left和right与其移动位置之后的元素进行比较,利用while,判断nums[left] == nums[left+1],nums[right] == nums[right - 1],若相等,left = left + 1,right = right -1,这里使用while循环,需注意要满足前提条件right>left,之后循环完返回结果result。时间复杂度O(n^2),空间复杂度O(1)。

        还有一个需要注意的点,在leetcode上,针对left和right的剪枝操作,需要left在后,right在前,否则会溢出报错。但我在本地的编译器上没有这样的问题。

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;
    }
};

四数之和

        参考上题的三数相加的双指针法,再加一层外循环。此外,需要考虑一些细节,首先,由于计算的target不是上题般恒为0,因此最开始的判断需要改变为

if(nums[i] > target && (nums[i] >=0 || target >= 0)){
            break;

此外,在第一层内循环时同样也要加入这样的判断

if(nums[j] + nums[i]>target && nums[i]+ nums[j]>=0){
                    break;
                }

也可不加,不过会减慢效率,此外,考虑到这个四数相加的结果可能超出int范围,将四数相加的结果保存为long,这里卡了我很久,如果不保存为long,leetcode大概会在280+停止。

 if ((long)nums[i] + nums[j] + nums[left] + nums[right] < target) {
                        left++;
                    } else if ((long)nums[i] + nums[j] + nums[left] + nums[right] > target) {
                        right--;
                    }

具体代码如下:

class Solution {
public:
    std::vector<std::vector<int>> fourSum(std::vector<int>& nums, int target) {
        std::sort(nums.begin(), nums.end()); // 先对数组进行排序
        std::vector<std::vector<int>> ans;
        for (int i = 0; i < nums.size(); i++) {
            // 跳过重复的数字
            if(nums[i] > target && (nums[i] >=0 || target >= 0)){
            break;
        }
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            for (int j = i + 1; j < nums.size(); j++) {
                // 跳过重复的数字
                if(nums[j] + nums[i]>target && nums[i]+ nums[j]>=0){
                    break;
                }
                if (j > i + 1 && nums[j] == nums[j - 1]) {
                    continue;
                }
                int left = j + 1, right = nums.size() - 1;
                while (left < right) {
                    if ((long)nums[i] + nums[j] + nums[left] + nums[right] < target) {
                        left++;
                    } else if ((long)nums[i] + nums[j] + nums[left] + nums[right] > target) {
                        right--;
                    } else {
                        ans.push_back({nums[i], nums[j], 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 ans;
    }
};

算法的空间复杂度O(1),时间复杂度O(n^3),具体解释如下

  1. 排序:首先对数组进行排序,这需要 O(nlog n) 的时间。

  2. 遍历数组:算法中有一个外层循环,它遍历数组中的每个元素作为四数之和的一个元素,这需要 O(n) 的时间。

  3. 内部循环:对于每个外层循环中的元素,有一个内层循环,它也遍历数组中的每个元素作为四数之和的一个元素,这同样需要 O(n) 的时间。

  4. 双指针:在内层循环中,我们使用双指针来找到和为 target - nums[i] - nums[j] 的两个元素,这需要 O(n) 的时间。

因此,总的时间复杂度是 O(n log n + n^2 + n^2) = O(n^3),其中 n 是数组的长度。在最坏的情况下,每个元素都可能被用作四数之和的一个元素,导致双指针操作的次数达到 n^2。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值