代码随想录算法训练营day07

题目:454.四数相加II、383. 赎金信、15. 三数之和、18. 四数之和

参考链接:代码随想录

454.四数相加II

思路:本题和两数之和感觉类似,四个数我第一想到的是将其拆分成a+b和c+d,由于只需要返回次数,我只需要将a+b的次数保存到哈希表m1中,c+d保存到m2中,然后在s2中找-a-b的次数,将两个哈希表的次数相乘得到结果。时间复杂度O(n^2)。

class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        unordered_map<int,int> m1,m2;
        int n=nums1.size();
        for(int i=0;i<n;i++){
            for(int j=0;j<n;j++){
                auto it=m1.find(nums1[i]+nums2[j]);
                if(it==m1.end()){//没有找到
                    m1[nums1[i]+nums2[j]]=1;
                }
                else{
                    m1[nums1[i]+nums2[j]]++;
                }
            }
        }
        for(int i=0;i<n;i++){
            for(int j=0;j<n;j++){
                auto it=m2.find(nums3[i]+nums4[j]);
                if(it==m2.end()){//没有找到
                    m2[nums3[i]+nums4[j]]=1;
                }
                else{
                    m2[nums3[i]+nums4[j]]++;
                }
            }
        }
        int sum=0;
        for(auto it1:m1){
            auto it2=m2.find(-it1.first);//找-a-b
            if(it2!=m2.end()){
                sum+=it1.second*it2->second;
            }
        }
        return sum;
    }
};

看了标答,发现还有许多简化空间。首先只需要一个map用来存a+b;然后对a+b的second一开始初始化就为0,所以直接用++就行,不需要额外来一个if;然后对数组遍历可以使用 int a:A 更快一些;对c+d的遍历,直接对每次-c-d在map出现的时候进行count的计数,不用额外把所有c+d的结果存放到一个单独哈希表中。
标答:

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

运行标答后时间从225ms减少到174ms。

383. 赎金信

思路:本题其实就是ransomNote中的每个字母数必须少于magazine中的每个字母数,可以使用长度为26的数组作为哈希表,用于存储字母数,首先存magazine的,然后再遍历ransomNote,遇到则–,最后查看表中有没有负数,如果没有则说明可以。时间复杂度O(n^2)。

class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
        int record[26]={0};
        for(int i=0;i<magazine.size();i++){
            record[magazine[i]-'a']++;
        }
        for(int i=0;i<ransomNote.size();i++){
            record[ransomNote[i]-'a']--;
        }
        for(int i=0;i<26;i++){
            if(record[i]<0){
                return false;
            }
        }
        return true;
    }
};

标答也做了细微简化,首先是一开始如果ransomNote长度更长,直接排除,然后在记录record的过程中就完成了bool值判断。
标答:

class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
        int record[26] = {0};
        //add
        if (ransomNote.size() > magazine.size()) {
            return false;
        }
        for (int i = 0; i < magazine.length(); i++) {
            // 通过record数据记录 magazine里各个字符出现次数
            record[magazine[i]-'a'] ++;
        }
        for (int j = 0; j < ransomNote.length(); j++) {
            // 遍历ransomNote,在record里对应的字符个数做--操作
            record[ransomNote[j]-'a']--;
            // 如果小于零说明ransomNote里出现的字符,magazine没有
            if(record[ransomNote[j]-'a'] < 0) {
                return false;
            }
        }
        return true;
    }
};

暴力解法使用erase操作,时间复杂度O(n^2):

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++) {
                // 在ransomNote中找到和magazine相同的字符
                if (magazine[i] == ransomNote[j]) {
                    ransomNote.erase(ransomNote.begin() + j); // ransomNote删除这个字符
                    break;
                }
            }
        }
        // 如果ransomNote为空,则说明magazine的字符可以组成ransomNote
        if (ransomNote.length() == 0) {
            return true;
        }
        return false;
    }
};

15. 三数之和

思路:本题使用哈希法比较困难,主要是去重过程不好处理,而两数之和的题目没有考虑去重问题。我也没想出更好的方法,看了答案才知道需要用到双指针法。
首先将数组排序,从小到大,快排时间复杂度O(nlogn),然后数组头放一个i用于遍历头,然后第二个位置和末尾分别放left和right,当三数之和小于0时,需要右移left,大于0,则左移right,直到两个相遇。时间复杂度O(n^2)。

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        int i,left,right,sum;
        vector<vector<int>> ans;
        sort(nums.begin(),nums.end());
        for(i=0;i<nums.size()-2;i++){//三个数不能重复
            left=i+1;
            right=nums.size()-1;
            while(left<right){
                sum=nums[i]+nums[left]+nums[right];
                if(sum==0){
                    ans.push_back({i,left,right});
                }
                else if(sum<0){
                    left++;
                }
                else{
                    right--;
                }
            }
        }
        return ans;
    }
};

但写完代码后运行提示超出内存限制,发现是自己没考虑去重问题,题目没看清楚。修改后代码:

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        int i,left,right,sum;
        vector<vector<int>> ans;
        sort(nums.begin(),nums.end());
        for(i=0;i<nums.size()-2;i++){//三个数不能重复
            if(nums[i]>0) {//首个元素大于0,不可能再有结果
                return ans;
            }
            if(i>0&&nums[i]==nums[i-1]){//对i去重,如果有两个相邻的则移动,注意不是nums[i]==nums[i+1]
                continue;
            }
            left=i+1;
            right=nums.size()-1;
            while(left<right){
                sum=nums[i]+nums[left]+nums[right];
                if(sum==0){
                    while(left<right && nums[left]==nums[left+1]){
                        left++;//移动到最右边的一个重复left
                    }
                    while(left<right && nums[right]==nums[right-1]){
                        right--;
                    }
                    ans.push_back({nums[i],nums[left],nums[right]});
                    left++;
                    right--;    
                }
                else if(sum<0){
                    left++;
                }
                else{
                    right--;
                }
            }
        }
        return ans;
    }
};

注意去重过程就好,尤其是对i的去重,必须是 if(i>0&&nums[i]==nums[i-1]) ,我写出的答案和标答几乎没有区别。

18. 四数之和

思路:本题也是需要判断出全部不重复的组合,所以使用哈希法并不合适,依旧是使用和上一题一样的双指针法,时间复杂度为O(n^3)。

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        int i,j,left,right,sum;
        vector<vector<int>> ans;
        sort(nums.begin(),nums.end());
        for(int i=0;i<nums.size()-3;i++){//实际上这里就写nums.size()也行,反正到不了
            if(nums[i]>target && nums[i]>=0){//这里剪枝必须保证nums[i]>=0,所以能剪的少一些
                return ans;
            }
            if(i>0&&nums[i]==nums[i-1]){
                continue;
            }
            for(int j=i+1;j<nums.size()-2;j++){
                if(nums[i]+nums[j]>target && nums[i]+nums[j]>=0){//剪枝
                    break;
                }
                if(j>i+1&&nums[j]==nums[j-1]){
                    continue;
                }
                left=j+1;
                right=nums.size()-1;
                while(left<right){
                    sum=nums[i]+nums[j]+nums[left]+nums[right];
                    if(sum==target){
                        while(left<right && nums[left]==nums[left+1]){
                            left++;
                        }
                        while(left<right && nums[right]==nums[right-1]){
                            right--;
                        }
                        ans.push_back({nums[i],nums[j],nums[left],nums[right]});
                        left++;
                        right++;
                    }
                    else if(sum<target){
                        left++;
                    }
                    else{
                        right--;
                    }
                }
            }
        }
        return ans;
    }
};

运行出现runtime error,发现错误是输入为[0],target=0时出错,错误原因是堆溢出。调试发现输入[0]时,nums.size()虽然为1,但nums.size()-3却变为随机数,经查询得知size()返回的是size_t类型,我们将其转换为int。故以后遇到需要使用size()来进行运算的,我们最好首先进行 n=nums.size()
然后还有一个问题是sum大小溢出,我们需要将sum定义成long类型,因为题目定义了nums[i]的范围为10^9,故需要定义long,以后需要注意观察题目要求的取值范围。然后是类型转换的具体写法,必须是 (long)nums[i]+nums[j]+nums[left]+nums[right] ,即将第一个数转换为long,后面的加法long+int会自动转换,如果写的 long(nums[i]+nums[j]+nums[left]+nums[right]) ,会报错,因为四个数相加的结果已经溢出了,再转换就晚了
修改后的答案:

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        int i,j,left,right;
        int n=nums.size();
        long sum;
        vector<vector<int>> ans;
        sort(nums.begin(),nums.end());
        cout << nums.size()-3 << endl;
        for(int i=0;i<n-3;i++){//实际上这里就写nums.size()也行,反正到不了
            if(nums[i]>target && nums[i]>=0){//这里剪枝必须保证nums[i]>=0,所以能剪的少一些
                return ans;
            }
            if(i>0&&nums[i]==nums[i-1]){
                continue;
            }
            for(int j=i+1;j<n-2;j++){
                if(nums[i]+nums[j]>target && nums[i]+nums[j]>=0){//剪枝
                    break;
                }
                if(j>i+1&&nums[j]==nums[j-1]){
                    continue;
                }
                left=j+1;
                right=nums.size()-1;
                while(left<right){
                    (long)nums[i]+nums[j]+nums[left]+nums[right];//必须这么写
                    if(sum==target){
                        while(left<right && nums[left]==nums[left+1]){
                            left++;
                        }
                        while(left<right && nums[right]==nums[right-1]){
                            right--;
                        }
                        ans.push_back({nums[i],nums[j],nums[left],nums[right]});
                        left++;
                        right--;
                    }
                    else if(sum<target){
                        left++;
                    }
                    else{
                        right--;
                    }
                }
            }
        }
        return ans;
    }
};

我写的甚至比标答还简洁些,主要是对两层for循环的边界做了一个缩小。

总结

对于哈希表,主要有数组、set、map三类,其中数组使用于取值范围固定的情况,set则适用于取值范围不固定,而map则需要记录其他信息,当除了记录key外,还需要记录value的时候,数组和set就不够用了,需要map,但能用前两个尽量别用map,空间占用大。在需要去重,十分麻烦的时候,使用双指针法有时会比哈希法更有效。

你可能感兴趣的:(算法,哈希算法)