C++ day07 哈希表 四数相加Ⅱ 赎金信 三数之和 四数之和

题目1:454 四数相加Ⅱ

题目链接:四数相加Ⅱ

对题目的理解

四个长度相同的整数数组,分别取不同的下标值时,其和为0,计算下标的组合个数。

自己的思路

思路1:遍历四个数组,将和依次求解计算有多少个组合,这样做冗余不推荐;

思路2:想到用哈希表来解决,因为数组元素数值可能很大,所以只考虑set或者map,

明确题目想求什么,因为给了A,B,C,D四个数组,所以求和的时候进行两两两分组,A,B是一组,C,D是一组,分别求和,同时将A与B元素之和放到哈希表中,使用C和D的和作为查询,要统计出0-(C+D)是否出现过,还要统计这个数出现了多少次,以此来计算满足的组合有几对,综上,需要统计两个数值,A与B内元素的和以及该和对应出现的次数,因次需要使用哈希表中的map,且使用效率最高的unordered_map,key:和  ,value:次数(find是找key存不存在)

注意:因为本题需要求解多少个元素之和满足,所以遇到和相等的情况下,不用去重

最后求解的个数用count计数,记得最后相加的是value。

具体解题步骤:

  1. 首先定义 一个unordered_map,key放a和b两数之和,value 放a和b两数之和出现的次数。
  2. 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中。
  3. 定义int变量count,用来统计 a+b+c+d = 0 出现的次数。
  4. 在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就用count把map中key对应的value也就是出现次数统计出来。
  5. 最后返回统计值 count 就可以了
  • 时间复杂度: O(n^2)
  • 空间复杂度: O(n^2),最坏情况下A和B的值各不相同,相加产生的数字个数为 n^2
class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        unordered_map<int,int> map ;//存储nums1和nums2的和
        int count = 0;
        for(int i=0;i<nums1.size();i++){
            for(int j=0;j<nums2.size();j++){
                map[i+j]++;//在和作为下标的位置处+1
            }
        }
        for(int i=0;i<nums3.size();i++){
            for(int j=0;j<nums4.size();j++){
                int target = 0 - (i+j);
                if(map.find(target)!=map.end())//说明找到了和为0的元素
                count+=map[target];
            }
        }
        return count;

    }
};

这样编程为什么会计算输出错误呢??

因为i+j处使用nums[i]和nums[j]才对啊,自己傻了,下面的是正确的代码

class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        unordered_map<int,int> map ;//存储nums1和nums2的和
        int count = 0;
        for(int i=0;i<nums1.size();i++){
            for(int j=0;j<nums2.size();j++){
                map[nums1[i]+nums2[j]]++;//在和作为下标的位置处+1
            }
        }
        for(int i=0;i<nums3.size();i++){
            for(int j=0;j<nums4.size();j++){
                int target = 0 - nums3[i]-nums4[j];
                if(map.find(target)!=map.end())//说明找到了和为0的元素
                count+=map[target];
            }
        }
        return count;
    }
};

下面的这段程序就不会出现错误,能正常出结果

class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        unordered_map<int,int> map ;//存储nums1和nums2的和
        int count = 0;
        for(int i:nums1){
            for(int j:nums2){
                map[i+j]++;//在和作为下标的位置处+1
            }
        }
        for(int i:nums3){
            for(int j:nums4){
                int target = 0 - (i+j);
                if(map.find(target)!=map.end())//说明找到了和为0的元素
                count += map[target];
            }
        }
        return count;
    }
};

题目2:383 赎金信

题目链接:赎金信

对题目的理解

判断两个字符串a,b中,a能不能用b中的字符表示,每个字符只能使用1次,且字符串均由26个小写字母组成。

自己的思路

想到a中的字符是不是在b中出现过,那么想到使用哈希表,因为字符均为小写字母,所以取值范围较小,所以想到使用哈希表中的数组。

自己的解法

用一个长度为26的数组来记录magazine里字母出现的次数。

然后再用ransomNote去验证这个数组是否包含了ransomNote所需要的所有字母。

  • 时间复杂度: O(n)
  • 空间复杂度: O(1)
class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
        int hash[26] = {0};
        if(ransomNote.size()>magazine.size()){
            return false;
        }//这行代码更加严密,因为每一个字母只能使用1次
        for(int i=0;i<magazine.length();i++){
            hash[magazine[i]-'a']++;
        }
        for(int i=0;i<ransomNote.length();i++){
            hash[ransomNote[i]-'a']--;
            if(hash[ransomNote[i]-'a'] < 0)//这里因为是能不能组成,所以可以大于0,但是不能小于0
                return false;
        }
        
        return true;
    }
};

暴力解法(不推荐)

  • 时间复杂度: O(n^2),时间复杂度是比较高的,而且里面还有一个字符串删除也就是erase的操作,也是费时的,
  • 空间复杂度: 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(magazine[i]==ransomNote[j]){
                   ransomNote.erase(ransomNote.begin()+j);
                   break;    
                }
           }
       }
        if(ransomNote.length()==0)
        return true;
        return false;
    }
};

题目3:15 三数之和

题目链接:三数之和

对题目的理解

整数数组中在不同下标位置处,其对应的数组之和为0,其中加和为0的组合中,若两个组合的元各个素相同,则只返回1组元素。

自己的思路

遍历整个数组求和再去重

两层for循环就可以确定 a 和b 的数值了,可以使用哈希法来确定 0-(a+b) 是否在 数组里出现过,其实这个思路是正确的,有一个非常棘手的问题,就是题目中说的不可以包含重复的三元组

把符合条件的三元组放进vector中,然后再去重,这样是非常费时的,很容易超时,去重的过程不好处理,有很多小细节

哈希的解法(不推荐)

  • 时间复杂度: O(n^2)
  • 空间复杂度: O(n),额外的 set 开销
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> result;
        sort(nums.begin(),nums.end());//将nums排序
        //找出a+b+c=0、a=nums[i],b=nums[j],c=-(a+b)
        for(int i=0;i<nums.size();i++){
            //排序之后如果第一个元素已经大于0,不可能满足要求等于0
            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]){
                    continue;
                }//b去重,b是紧挨着a的下一个元素
                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;

    }
};

双指针解法

其实这道题目使用哈希法并不十分合适,因为在去重的操作中有很多细节需要注意,哈希法的执行时间比较长,这道题使用双指针解法更高效一些

给定如下一个数组

使用双指针法首先对数组进行排序,使用sort进行排序,进行一个for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上

还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i],b = nums[left],c = nums[right]

首先判断第一个元素nums[i]>0,如果第一个元素已经大于0了,那么后面的两个元素不用看了,(因为顺序已经排好了),所以直接return就好,后面的元素一定都大于a>0,和也一定都大于0。

在什么情况下循环查询呢?left<right     OR      left<=right   究竟是哪一个呢?

如果是left<=right的话,那么就证明可能存在left=right的情况,而题目要求,a,b,c三个数下标不等,就不可能指向同一个位置,因此循环条件应该是left<right。

如何移动left 和right呢, 如果nums[i] + nums[left] + nums[right] > 0 就说明 此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,right--,这样才能让三数之和小一些。

如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,left++,才能让三数之和大一些,直到left与right相遇为止。

!!!!!!!!本题的重点在于去重,即a,b,c三个数的去重!!!!!!!

a的去重:a是nums[i]中遍历的元素,如果重复应该直接跳过

!!!但是这里有一个问题,是判断 nums[i] 与 nums[i + 1]是否相同,还是判断 nums[i] 与 nums[i-1] 是否相同,这两者的含义大为不同,nums[i-1]是nums[i]与前一个元素进行比较,nums[i+1]是nums[i]与后一个元素(left)进行比较,如果是nums[i]==nums[i+1]把 三元组中出现重复元素的情况直接pass掉了。 例如{-1, -1 ,2} 这组数据,当遍历到第一个-1 的时候(i),判断 下一个也是-1(left),那这组数据就pass了。

要做的是 不能有重复的三元组,但三元组内的元素是可以重复的!

因此,需要当前使用 nums[i],判断前一位nums[i-1]是不是一样的元素,在看 {-1, -1 ,2} 这组数据,当遍历到 第一个 -1 的时候,只要前一位没有-1,那么 {-1, -1 ,2} 这组数据一样可以收录到 结果集里。

b的去重

一定要是while循环,不能使用if,这样可以排除掉部分相同的情况,是和(nums[left+1])比较判断。

c的去重

一定要是while循环,不能使用if,这样可以排除掉部分相同的情况,是和(nums[right-1])一样比较判断

最后也是中最重要的一点:整个代码的逻辑,

i)a的去重放在while(left<right)之前,因为它是三个元素当中的第一个元素,所以首先需要对a进行去重,

ii)就是是先去重还是先判断取结果的过程?

如果是先去重的话,那么一个特殊的元素组合{0,0,0,0,0,0},就会一直进行去重,最终跳出循环,得不到任何结果。所以我们至少要得到一个结果,再对b和c进行去重。

获得结果之后,记得要同时移动left和right,这样才能继续向下判断。

vector<vector<int>>是一个二维向量,也可以称之为嵌套向量或二维动态数组。它由若干个一维向量组成,每个一维向量又包含若干个整数元素。可以将其看做是一个矩阵。

  • 时间复杂度: O(n^2)
  • 空间复杂度: O(1)
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) return result;//因为nums[i]作为3个中的第一个元素,所以如果nums[i]大于0,和一定大于0
             if(i>0 && nums[i]==nums[i-1]) continue;//对a去重,一定要是nums[i]==nums[i-1]
            int left = i+1;
            int right = nums.size()-1;
            while(left<right){//循环条件一定要是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(vector<int>{nums[i],nums[left],nums[right]});               
                    while(left<right && nums[left]==nums[left+1]) left++;//对b去重  一定要是while 使用while可以连续排除相同的元素,而if就不可以
                    while(left<right && nums[right]==nums[right-1]) right--;//对c去重
                    left++;//这里又加了,对于有重复的元素,可以跳过b和c相同的元素;对于没有重复的元素,可以移动left和right
                    right--;
                }
            }             
        }
        return result;
    }
};

思考

既然三数之和可以使用双指针,那两数之和呢?

前天的题目求解两数之和要求返回索引下标,但双指针法一定要排序,这时候原数组的索引就改变了,所以不可以使用,但是如果两数之和要求返回的是数值的时候双指针法才适用。

题目4:18四数之和

题目链接:四数之和

对题目的理解

数组nums大小是n,由整数组成,要求下标不同的4个元素的加和等于目标值target,而且不同的加和组合之间元素不能完全相同。


自己的思路

想到三数之和,将思想迁移过来。

双指针解法

四数之和与三数之和是一个思路,都是使用双指针法, 基本解法就是在三数之和的基础上再套一层for循环。

首先排序,然后进行位置处的赋值,与三数之和相比,多了一个变量k。

此时,需要进行一系列的剪枝和去重操作,因为限制多了

但是有一些细节需要注意,

i)对于k而言,不能判断nums[k] > target 就返回了,三数之和 可以通过 nums[i] > 0 就返回了,因为 0 已经是确定的数了,四数之和这道题目 target是任意值。比如:数组是[-4, -3, -2, -1]target-10,不能因为-4 > -10而跳过。但是我们依旧可以去做剪枝,剪枝逻辑变成nums[i] > target && nums[i] > 0 &&target > 0,同时也必须break,也是因为这个target是任意值的原因,如果直接return result的话,会丢失部分结果,因为有的数值组合直接跳过了;去重逻辑是没有变,k>0&&nums[k]==nums[k-1]。

ii)对于i而言,也存在着剪枝逻辑,nums[i]+nums[k]>target&&nums[i]+nums[k]>0&&target>0

去重逻辑有一丝丝的变化,就是i的范围这里,因为有了k的存在,所以i会受到一些范围上的限制,i>k+1&&nums[i]==nums[i-1]

iii)其他的步骤没有变化

四数之和的双指针解法是两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下标作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target的情况。

  • 时间复杂度: 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++){
            //对k进行剪枝去重
            if(nums[k]>target && nums[k]>0 && target>0) return result;//一级剪枝
            if(k>0 && nums[k]==nums[k-1]) continue;//二级去重
            for(int i=k+1;i<nums.size();i++){
                if(nums[i]+nums[k]>target && nums[i]+nums[k]>0 && target>0) return result;//二级剪枝
                if(i>k+1 && nums[i]==nums[i-1])  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 {
                        result.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 result;

    }
};

注意:在剪枝后面直接return 会造成结果的丢失,由于这个是一个整数数组,所以符号是不定的,因此直接使用break,最后再统一进行return。

下面是使用break没有报错的正确代码:

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++){
            //对k进行剪枝去重
            if(nums[k]>target && nums[k]>0 && target>0) break;//一级剪枝
            if(k>0 && nums[k]==nums[k-1]) continue;//二级去重
            for(int i=k+1;i<nums.size();i++){
                if(nums[i]+nums[k]>target && nums[i]+nums[k]>0 && target>0) break;//二级剪枝
                if(i>k+1 && nums[i]==nums[i-1])  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 {
                        result.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 result;

    }
};


 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值