6.哈希表(2) | 四数相加II、赎金信、三数之和、四数之和

        今天的四道题着实费了不少功夫, 只有第2题比较简单。第1题在看过题解后实现上并不难,第3、4题不仅剪枝思路需要考虑周到,实现也容易出错。第3题的哈希方法版题解着实很难理解,相比之下双指针法要容易很多。第1、4题,第3、4题之间都有相似之处,需要对比它们的不同之处。


        第1题(454.四数相加II)在冥思苦想后,得到的思路是对4组数分别两两配对,比如第一次1、2,3、4分别配对,第2次1、3,2、4分别配对······总共配对3次。每次配对后的两组分别进行双重循环,从第一组的2个小组中分别取2个数取和放到unordered_map中,再在第二组的2个小组双重循环时取和,在unordered_map中查找其相反数,从而实现O(n²logn)的时间复杂度。

        看了题解后发现基本思路没问题,但考虑得过于复杂了。因为四数之和等于0的话,那么不论如何分组,分别对组内两数字求和,得到的两个结果都必定为相反数。也就是说,我自己思路中的第一次配对,已经涵盖了所有可能的情况,所以只需要1、2组,3、4组分别配对即可,无需后面的多次配对。

#include<unordered_map>

class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        unordered_map<int, int> map;
        for (int a : nums1) {
            for (int b : nums2) {
                map[a + b]++;
            }
        }
        int ans = 0;
        for (int c : nums3) {
            for (int d : nums4) {
                auto it = map.find(0 - c - d);
                if (it != map.end()) {
                    ans += it->second;
                }
            }
        }
        return ans;
    }
};

从题解代码中学习到了遍历vector比较方便的方法,如代码7、8、13、14行。

        二刷:没有用map,错用成了set,导致漏了(a + b)想等的重复值。


        第2题(383. 赎金信)比较简单,只需先统计出magazine中各字符出现次数,再遍历ransomNote,将对应字符的次数减一,再判断次数是否小于0即可。在实现过程中才意识到这个题目同242.有效的字母异位词一样并不需要unordered_map,只需长度为26的int数组即可。

#include<unordered_map>
class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
        int map[26] = {0};
        for (char c : magazine) {
            map[c - 'a']++;
        }
        for (char c : ransomNote) {
            if (--map[c - 'a'] < 0) {
                return false;
            }
        }
        return true;
    }
};

        二刷:忘记判断map中字符对应数字是否等于0。


        第3题(15. 三数之和)是近期遇到的最难的一道题,只能相当双重循环的哈希法,但这个方法的去重部分还是不会。看过哈希法的题解后,发现这个问题的关键是去重,总结为以下几个去重的关键点:

  • a(三个数中第一个)的去重有2种写法,应该用哪种?
if (nums[i] == nums[i + 1]) {
    continue;
}

这种写法在连续的a的末尾才开始尝试a,之前的部分就会被略过,所以面对{-1, -1 ,2}这种情况时就会出错。正确的写法应该是在连续的a的开始就尝试a,再忽略掉以后的:

if (i > 0 && nums[i] == nums[i - 1]) {
    continue;
}
  • b的去重应该和a一样还是有所区别?(这里是思考了许久的关键问题)因为b不仅涉及自身,还是c所在范围的开始,b和c可能相等(如a,b,c分别为-2,1,1),所以如果使用与a相同的去重方法的话,就会错误去重掉b和c相等且符合要求的情况,所以在连续3次重复时才跳过,而非连续2次重复就跳过。
  • c的去重看似不需要也不应该,但c与b是相关的,在a确定的情况下,如果c取了重复的值,那么b的取值也一定是与之前重复的,所以有必要对c进行去重。

        解决了去重的难题后的代码如下:

#include<algorithm>
#include<unordered_set>

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        vector<vector<int>> ans;
        int n = nums.size();
        for (int i = 0; i < n - 2; ++i) {
            if (nums[i] > 0) {
                return ans;
            }
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            else if (nums[i] + nums[i + 1] + nums[i + 2] > 0) {
                continue;
            }
            else if (nums[i] + nums[n - 2] + nums[n - 1] < 0) {
                continue;
            }
            unordered_set<int> set;
            for (int j = i + 1; j < n; ++j) {
                if (j > i + 2 
                    && nums[j] == nums[j - 1] 
                    && nums[j - 1] == nums[j - 2]) { // b和c可能相等(如a,b,c分别为-2,1,1),所以第3个重复时才跳过
                    continue;
                }
                int c = 0 - nums[i] - nums[j];
                if (set.find(c) != set.end()) {
                    ans.push_back({nums[i], nums[j], c});
                    set.erase(c);
                }
                else {
                    set.insert(nums[j]);
                }
            }
        }
        return ans;
    }
};

        哈希法的题解代码中还有2个需要关注的地方。第1个是剪枝,在外层循环一开始时,可以利用已经排好序的数组的边界和当前a的值进行一些简单判断,达到剪枝的目的。第2个是注意vector和数组在sort()的使用上有所不同,传入的参数是.begin()和.end()。

        哈希法最终的时间,空间消耗都较高,且实现复杂去重非常容易出错,所以双指针法才是正解。双指针法自己同样想不出来,只得去看题解。双指针的思路是外层循环遍历a(与哈希法一样需要去重),内层制定指向b,c的左右指针,初始时分别指向a的右邻居和数组最后一位,再根据a、b、c三者的和与0的关系分别进行右移和左移。和恰好等于0时就保存一个答案并对b,c进行去重操作。代码如下:

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        vector<vector<int>> ans;
        int n = nums.size();
        for (int i = 0; i < n - 2; ++i) {
            if (nums[i] > 0) {
                return ans;
            }
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            else if (nums[i] + nums[i + 1] + nums[i + 2] > 0) {
                continue;
            }
            else if (nums[i] + nums[n - 2] + nums[n - 1] < 0) {
                continue;
            }
            int l = i + 1, r = n - 1;
            while (l < r) {
                if (nums[i] + nums[l] + nums[r] < 0) {
                    ++l;
                }
                else if (nums[i] + nums[l] + nums[r] > 0) {
                    --r;
                }
                else {
                    ans.push_back({nums[i], nums[l], nums[r]});
                    l++;
                    r--;
                    while (l < r && nums[l] == nums[l - 1]) {
                        l++;
                    }
                    while (l < r && nums[r] == nums[r + 1]) {
                        r--;
                    }
                }
            }
        }
        return ans;
    }
};

 时间和空间消耗相比哈希法都大大减少,但还有些需要注意的地方:

  • 三者和等于0时,添加完答案后,需要在此时对b,c去重。其他2种情况的去重不必要,因为即便不去重,按照代码逻辑也会在下次循环时进行指针移动操作。
  • 对b,c去重时,循环条件里也需像外层循环一样加上l < r,否则可能导致数组越界。

        二刷:没想到解法。


        第4题(18. 四数之和)相比于第1题(454.四数相加II)的不同之处在于:4个数组变为1个数组,所以需要去重,哈希方法不再合适;

        相比于第3题(15. 三数之和),除了3个数变为4个数之外的不同在于:目标和不再是0。

        这道题也直接看了视频题解,整体思路与第3题一致,多了一层循环的同时在剪枝部分稍有不同。由于上面的第2个不同,循环中的当前位置之后还可能包含负数,所以在剪枝时需要确保当前数字 >= 0,在此基础上再判断当前数字与target的关系来决定是否剪枝(17~19,31~33行),其改进版的剪枝也是如此(13~16,27~30行)。

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        sort(nums.begin(), nums.end());
        vector<vector<int>> ans;
        int n = nums.size();
        for (int i = 0; i < n - 3; ++i) {
            if (i > 0 && nums[i] == nums[i - 1]) { // 去重
                continue;
            }
            if ((long) nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] >= 0 
                && (long) nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) { // break型剪枝
                break;
            }
            // if (nums[i] >= 0 && nums[i] > target) { // break型剪枝(被上面的break型剪枝所包含)
            //     break;
            // }
            if ((long) nums[i] + nums[n - 3] + nums[n - 2] + nums[n - 1] < target) { // continue型剪枝
                continue;
            }
            for (int j = i + 1; j < n - 2; ++j) {
                if (j > i + 1 && nums[j] == nums[j - 1]) { // 去重
                    continue;
                }
                if ((long) nums[i] + nums[j] + nums[j + 1] + nums[j + 2] >= 0 
                    && (long) nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target) { // break型剪枝
                    break;
                }
                // if (nums[i] + nums[j] >= 0 && nums[i] + nums[j] > target) { // break型剪枝(被上面的break型剪枝所包含)
                //     break;
                // }
                if ((long) nums[i] + nums[j] + nums[n - 2] + nums[n - 1] < target) { // continue型剪枝
                    continue;
                }
                int l = j + 1, r = n - 1;
                while (l < r) {
                    if ((long) nums[i] + nums[j] + nums[l] + nums[r] < target) {
                        l++;
                    }
                    else if ((long) nums[i] + nums[j] + nums[l] + nums[r] > target) {
                        r--;
                    }
                    else {
                        ans.push_back({nums[i], nums[j], nums[l], nums[r]});
                        l++;
                        r--;
                        while (l < r && nums[l] == nums[l - 1]) {
                            l++;
                        }
                        while (l < r && nums[r] == nums[r + 1]) {
                            r--;
                        }
                    }
                }
            }
        }
        return ans;
    }
};

代码实现过程也并不顺利,踩了下面几个坑:

  • 错把continue型的剪枝写成了break。说明剪枝时用break还是continue很重要,容易出错(在这里耗了不少时间)。
  • 4个数字相加时会溢出int范围,需要转为long。这里学习到在两int数字比较大小时,可以直接把可能溢出的一个转为long,另一个int可以不用转。

        二刷:continue和break没注意分清楚。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值