Day6. 哈希表 454.四数相加Ⅱ 383.赎金信 15.三数之和 18.四数之和

92 阅读7分钟

454.四数相加Ⅱ

难度指数:😀😐😕

相对于四数之和,简单在:无需考虑去重的操作。

举个例子:如果数组里面所有的元素都是0,那么我们想要寻找的四元组就是[0,0,0,0],[0,0,0,0],……,[0,0,0,0]

虽然四元组都是[0,0,0,0],但是不同的0取自于数组里的不同位置,因此这道题 无需去重

在一个集合里面,需要判断一个元素有没有出现过,就要用哈希法。

暴力:

4个for循环遍历4个数组,取不同元素相加,如果 = 0,count++,最后返回count。 时间复杂度O(n^4)


哈希表:

可以只遍历这2个数组,

在遍历完A和B数组的时候,可以把 a + b 的值放到一个集合里;然后在判断C、D数组的时候,判断集合里有没有我们想要的元素,如果有,说明我们找到了一个匹配项 target = 0 ,这时就可以 count++ 了。

⚠️这道题元素的数量可能很大(int),用数组下标来做映射,肯定不够;只能考虑 set 或者 map

不仅要统计 a + b 在这个集合里有没有出现过,还要统计出现过多少次,然后才能和下面的 c + d 做一个映射。

如果使用的数据结构是 set,我们只有一个 key 去存是否出现过;然而,出现过的次数,我们还需要一个 value 来存,因此考虑用 map

整个题目的解题思路:

在遍历A、B数组的时候,将 a + b 放到map集合里面的key,同时统计 a + b出现过的次数。

然后在遍历C、D数组的时候,判断 0 - (c + d) 有没有在map集合里面出现过;如果出现过,就把出现的次数做一个统计。

说一嘴:0 - (c + d) = a + b

Q:为什么要先遍历A、B数组,然后再遍历C、D数组? O(n^2) + O(n^2),整体就是O(n^2)

我只先遍历A数组,然后遍历B、C、D数组不行吗?

A:不可以! 遍历后面的B、C、D,时间复杂度是O(n^3)

O(n^2) 肯定比 O(n^3) 更优。

AC代码: (核心代码模式)

 class Solution {
 public:
     int fourSumCount(vector<int>& A, vector<int>& B, vector<int>& C, vector<int>& D) {
         unordered_map<int, int> umap;  //key, value
         //遍历A、B数组,统计两个数组元素之和,以及出现的次数,放到map中
         for (int a : A) {
             for (int b : B) {
                 umap[a + b]++;
             }
         }
         int count = 0;  //统计a + b + c + d = 0 出现的次数
         for (int c : C) {
             for (int d : D) {
                 if (umap.find(0 - (c + d)) != umap.end()) {
                     count += umap[0 - (c + d)];
                 }
             }
         }
         return count;
     }
 };

383.赎金信

暴力解法:

AC代码: (核心代码模式)

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);  //ransomNote删除这个字符
                   break; 
                }
            }
        }
        //若ransomNote为空,说明magazine的内容可以组成ransomNote
        if (ransomNote.length() == 0) {
            return true;
        }
        return false;
    }
};

哈希解法:

AC代码: (核心代码模式)

class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
        int record[26] = {0};
        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++) {
            record[ransomNote[j] - 'a']--;

            if (record[ransomNote[j] - 'a'] < 0) {
                return false;
            }
        }
        return true;
    }
};

15.三数之和

难度指数:😀😕🙁

虽然可以用哈希法,但是用哈希法就整复杂了。

在一个数组中找出3个元素等于0,把这个三元组都找出来。 (⚠️三元组是去重的)

本题相对于"两数之和"的复杂就体现在:这道题需要去重

找出3个数,就是在数组里面找出 a + b + c = 0;

可以用2层for循环去遍历:第一层确定a,第二层确定b;

想找c的话就看 0 - (a + b) 这个值是否出现在数组里面:

 for (a   ) {
     for (b   ) {
         0 - (a + b)
     }
 }

如果有,就找到了一对相加结果为0的 a、b、c

要想寻找0 - (a + b)这个数值是否在数组里是否出现过,就要用哈希法(用 map 做映射)。

a需要去重,b需要去重,c也需要去重。

哈希解法:

不太建议用哈希法来做这道题,因为去重的细节太多了,很难做到bug free,基本上都会遇到点小问题,也很难一次想周全。

双指针解法:

更易于理解。

使用双指针法之前,一定要对数组先进行排序。

让我们找元素值的这个三元组相加等于0,没让我们返回下标。如果让你返回下标,你在一番排序之后,这些下标就都乱了。

06.01.png

 if (nums[i] + nums[left] + nums[right] > 0) {  //若3个数相加大于0
     right--;  //需要把3数之和变小,让right前移
 }
 if (nums[i] + nums[left] + nums[right] < 0) {  //若3个数相加小于0
     left++;  //需要把3数之和变大,让left后移
 }
 if (nums[i] + nums[left] + nums[right] == 0) {
     加进result数组
 }

深入细节:🦄去重是关键

a需要去重,b需要去重,c也需要去重。

(说人话:结果集里面有一个[1, 2, 3]了,就不能再出现[1, 2, 3]了。)

伪代码:

定义一个二维数组 result 来存放结果集。

 sort(nums);  //对输入的数组进行排序
 ​
 //遍历数组
 for (int i = 0; i < nums.size(); i++) {
     if (nums[i] > 0) return;  //第一个数大于0,那不用玩了(后面不管怎么收集也不可能3个数相加等于0)
     
 }

因为我们已经遍历了nums[i],就取了a这个数,要对a进行去重

⚠️注意细节:

Q:nums[i] == nums[i + 1];nums[i] == nums[i - 1]; 选哪个?

A:选择后者

AC代码: (核心代码模式)

 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++) {
             //排序后如果第一个元素大于0,那不用玩了
             if (nums[i] > 0) {
                 return result;
             }
             if (i > 0 && nums[i] == nums[i - 1]) {  //三元组元素a去重
                 continue;
             }
             int left = i + 1;
             int right = nums.size() - 1;
             while (right > 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;
     }
 };

18.四数之和

难度指数:😀😕🙁

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

 for (int k) {
     for (int i) {
         
     }
 }

k和i是确定的,还是靠leftright不断向中间移动,寻找到合适的四元组,使之相加等于target

在三数之和的代码基础上,外面套上一层for循环。

06.02.png

 for (int k) {
     for (int i) {
         "三叔之和"的代码 (不是原封不动地复制)
     }
 }
 nums[k] + nums[i] + nums[left] + nums[right] = target;

还是有很多小细节的,主要细节在于剪枝去重的部分:

剪枝操作:

如何对k进行剪枝操作?

在上一题中,如果nums[i] > 0,就直接进行剪枝的操作了。

那么,我们就会以为这道题,当nums[k] > target 就可以进行剪枝操作,其实不行!

因为target是你输入的一个数,它可以是正数,也可以是负数。

⚠️注意不要惯性思维:如果全是正数,两个数相加确实可以变得更大;但如果存在负数,那么两个数相加就有可能变得更小。

eg:[-4, -1, 0, 0] 是按从小到大排序

如果target是-5,

那么这个四元组就符合条件了。

你要是写代码:

 if (nums[k] > target) {
     break;
 }

这样就错过上面这个结果集了。

那么明白了这一点,这道题的剪枝操作就是:

也是可以做判断

if (nums[k] > target && nums[k] > 0 && target > 0) {
    break;  //这样就直接在这里做了剪枝
}

这里的 target > 0 也是可以不加的,只要nums[k] > 0 就避免了负数相加的情况。

去重操作:

if (k > 0 && nums[k] == nums[k - 1]) {
    continue;
}

进入for (int i) { }

还要对nums[i]进行剪枝和去重操作:

剪枝:

for (int i = k + 1; i < nums.size(); i++) {
    if (nums[k] + nums[i] > target && nums[k] + nums[i] > 0 && target > 0) {
        break;
    }
}

去重:

if (int i > k + 1 && nums[i] == nums[i - 1]) {
    continue; 
}

AC代码: (核心代码模式)

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++) {
            //剪枝处理
            if (nums[k] > target && nums[k] >= 0) {
                break;
            }
            //对nums[k]进行去重
            if (k > 0 && nums[k] == nums[k - 1]) {
                continue;
            }

            for (int i = k + 1; i < nums.size(); i++) {
                //2级剪枝处理
                if (nums[k] + nums[i] > target && nums[k] + nums[i] >= 0) {
                    break;
                }
                //对nums[i]进行2级去重
                if (i > k + 1 && nums[i] == nums[i - 1]) {
                    continue;
                }

                int left = i + 1;
                int right = nums.size() - 1;
                while (right > left) {
                    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]});

                        //对nums[left]和nums[right]去重
                        while (right > left && nums[right] == nums[right - 1]) right--;
                        while (right > left && nums[left] ==nums[left + 1]) left++;

                        //找到答案时,双指针同时收缩
                        right--;
                        left++;
                    }
                }
            }
        }

        return result;   
    }
};