[LeetCode] 四数和值问题类型总结(哈希、双指针)

写在前面

四数问题以及三数和值问题一般用Brute Force OJ会TLE,面试过程中写出BF算法也不是面试官想看到的,那么我们可以思考一下四数和值问题本质是在考察什么,表面是和值问题,本质是查找问题,对于查找,一般我们会联想到下面几点知识,i.e.,思考方向:

  • 查找问题最佳时间复杂度为二分算法对应的O(lgn),而二分查找一般与排序紧密相连,因此和值问题可以转化为排序+二分;
  • 四数问题需要查找4个值,想法1是定住3个数去二分查找1个数,而本想法是定住2个数,然后查找另外2个数,遵循查找问题先做排序的原则,排序之后,四数之和问题转为有序序列两数之和问题,这里一步可能不太好想到用Two pointer转化问题,而用Two pointer解题的关键是,两指针如何移动(可能同时移动,也可能只移动其一)。

454. 四数相加 II

原题链接

解题思路: 本题虽然是题18的扩展题,但是显然比它简单很多。这题主要考察哈希,我在写在前面点了一下面对四数问题的我们可能会采取的策略,此题可能思路又要变一变,我们可以这么思考,首先四数问题我们可以想到用BF解,但是BF最大的问题是时间复杂度太高,那么如果转换为查找为题,时间复杂度虽然降低一些,但依然无法接受,i.e. O ( n 3 l g n ) O(n^3lgn) O(n3lgn),降低时间复杂度常用方法我们思维备用箱里应该还要能想到空间换时间以降低时间复杂度的原则,OK,想到这一步基本就能想出来要结合hashmap解题,因为题目给定的list是4个互不干扰的list,且没有要求组合不能重复,那么前查找问题时间复杂度就可以降低为 O ( n 3 ) O(n^3) O(n3),(本质上这里采取的也是解题策略中非常常见也重要的想法,查找数组消耗的时间复杂度为O(n),若有序,时间复杂度为O(lgn)),但是若事先将数组存入hash中,对应查找密集型场景,hash时间复杂度为O(1),能很好地消除O(n)时间复杂度),那么至此还有没有更优的解法呢,我们分析一下前面的做法本质是3+1组合,而 O ( n 3 ) O(n^3) O(n3)正是前3导致的,那么若采取2+2组合,时间复杂度是否可以降低呢,OK,确实是这样,我们先计算A+B能组合出来的和值及其对应个数,然后在计算C+D时分别判断这个和值相反数是否存在,若存在则加上相应个数。

// 考察hash,将4个列表分成两组,先将A和B和值及其对应个数计算出来,
// 然后在计算C和D和值时,判断此和值在hash中是否存在,若存在则加上
// 对应个数
// T:O(n^2), S: O(n)
class Solution {
public:
    int fourSumCount(vector<int>& A, vector<int>& B, vector<int>& C, vector<int>& D) {
        unordered_map<int, int> m;
        int res = 0;
        for (auto &a : A) {
            for (auto &b : B) {
                ++m[a + b];
            }
        }
        for (auto &c : C) {
            for (auto &d : D) {
                res += m[-(c+d)];
            }
        }
        return res;
    }
};

18. 四数之和

原题链接

解题思路: 本题给定的数组个数只有一个,在这个数组中找四数之和等于目标值,此题与题454最大的区别是只给一个数组,在这个数组里找4个数,那么这4个数之间肯定会互相干扰,不能随意选,那么在用BF解时,依然会遇到上面时间复杂度分析问题,但是hashmap不能work了(因为四数存在依赖),但是我们可以在BF基础上进行改进,先确定两个数,另外两个数用Two pointer解,基本思路到这儿题目就解出来了,而且时间复杂度为 O ( n 3 ) O(n^3) O(n3)是可以AC的。目前要求组合不能重复,在解决这个问题时,我解出正确代码之前遇到1个坑点,这里记录一下:

  • 在过滤重复值时,需要在找到一个组合后再进行双指针移动过滤,若提前过滤,在有些test case中可能会漏解,e.g., 和值为4,序列为2,2,3,5,因为left指针为了去重会移动到第二个2上,导致right指针无法移动2上。

请看代码:

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        int n = nums.size();
        vector<vector<int>> res;
        sort(nums.begin(), nums.end());
        for (int i = 0; i < n; ++i) {
            if (i != 0 && nums[i] == nums[i - 1]) continue;
            for (int j = i + 1; j < n; ++j) {
                if (j != i + 1 && nums[j] == nums[j - 1]) continue;
                int left = j + 1, right = n - 1, t = target - (nums[i] + nums[j]);
                while (left < right) {
                    int sum = nums[left] + nums[right];
                    if (sum == t) {
                        res.push_back({nums[i], nums[j], nums[left], nums[right]});
                        ++left;
                        --right;
                        while (left < right && nums[left] == nums[left - 1]) ++left;
                        while (right > left && nums[right] == nums[right + 1]) --right;
                    } else if (sum < t) ++left;
                    else --right;
                } 
            }
        }
        return res;
    }
};

————————————

写在后面

  • 四数问题一般用BF解法解肯定能解出来,但是OJ会因时间复杂度过高不AC,而且面试官也不想看到这么直接的答案(无技术含金量),那么肯定是要想办法去做时间复杂度优化的,对于优化算法时间复杂度的方法,常规的是以空间换时间,这是一个非常朴素的原则,在很多地方都会用到,比如密集读场景下哈希查找效率优越,比如回溯和图遍历过程的记忆数组memo。而具体到四数问题,我们可以联想到的解法或者考点是哈希Two pointer
  • 这个点是写给LT题的感受,对于LT题,经常会在互联网面试中作为手撕代码的模板题或者在此基础上改进题,而通常情况下人们可能会选择去记忆题,其实这个方法并不恰当,我们在做LT题时,最好的方式,我认为是拿到题后,先分析题目要考什么,i.e.,分析考察点,这有两点好处,1)可以写标准算法或数据结构的解题模板,2)分析出考点后,更有利于我们增强信心做对此题,因为LT题一般考点就那些,如果能分析出考点,解题方向基本就对了,对代码做稍微调试即可AC。

你可能感兴趣的:(双指针,哈希)