【代码随想录 | day07】(JavaScript) 哈希表第二弹:四数相加II、赎金信、三数之和、四数之和

82 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

  • 454.四数相加II
  • 383.赎金信
  • 15.三数之和
  • 18.四数之和

454.四数相加II

题目链接:454. 四数相加 II - 力扣(LeetCode)

给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足: 0 <= i, j, k, l < n nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0

本题是哈希表的一道经典题目

  • HashMap存两个数组之和,如AB。然后计算两个数组之和,如 CD。时间复杂度为:O(n^2)+O(n^2),得到 O(n^2).
  • 将四个数组分为两块通过map记录A + B的值,及出现的次数(用 val 存起来);
  • 在遍历C和D时,判断0 - (C + D)是否在map中存在;

这道题应该思考的重点就是,遇到这种题目为什么会想到用哈希表。(需要查询元素)

代码

 var fourSumCount = function(nums1, nums2, nums3, nums4) {
     const twoSumMap = new Map();
     let count = 0;
 ​
      // 通过map记录nums1 + nums2的值
     for(const n1 of nums1) {
         for(const n2 of nums2) {
             const sum = n1 + n2;
             twoSumMap.set(sum, (twoSumMap.get(sum) || 0) + 1)
         }
     }
     
     // 判断 0 - (nums1 + nums2)是否在map中存在
     for(const n3 of nums3) {
         for(const n4 of nums4) {
             const sum = n3 + n4;
             count += (twoSumMap.get(0 - sum) || 0)
         }
     }
 ​
     return count;
 };

借用代码示例中的数据来解释一二。

nums1 = [1, 2], nums2 = [-2, -1], nums3 = [-1, 2], nums4 = [0, 2]

  • twoSumMap.set(sum, (twoSumMap.get(sum) || 0) + 1) 解读: 刚开始 sum = 1 + (-2) 时, 存的键为两数之和 -1 ,值代表了这个和出现的次数,这样之和需要查询 key 时,就知道了出现了几次。 补充一点

     map1.set('bar');
     console.log(map1.get('bar'));  // undefined 
     
     console.log((map1.get('bar') || 0) + 1);  
     // 值为1  其中 (undefined || 0) 值为0
    

    set()方法Map 对象添加或更新一个指定了键(key)和值(value)的(新)键值对。

  • twoSumMap,即 Map(3) { -1 => 1, 0 => 2, 1 => 1 }

  • 第二个双重 for 循环中,count += (twoSumMap.get(0 - sum) || 0)是什么意思?

    解读:已知 count 初始值为 0,当能找到0-(nums3 + nums4)时,自然要看符合条件的两数之和有几个。恰好哈希表里面已经统计好了,只有在循环的过程中不断更新 count 的值就可以了。


383.赎金信

题目链接:383. 赎金信 - 力扣(LeetCode)

注意:

  • magazine 中的每个字符只能在 ransomNote 中使用一次
  • ransomNotemagazine 由小写英文字母组成

这道题目和242.有效的字母异位词 (opens new window)很像,有效的字母异位词 相当于求 字符串a 和 字符串b 是否可以相互组成 ,而这道题目是求字符串a能否组成字符串b,而不用管字符串b 能不能组成字符串a。

这一题不推荐使用双重for循环的暴力解法,时间复杂度比较高,而且里面还有一个字符串删除也是费时的。

由于字母全是小写的,考虑用哈希解法,用空间换时间效率。这里得回顾一下,怎么将26个字母变成哈希表了—— const resSet = new Array(26).fill(0);这里采用数组而不是map。

要注意的是,将 magazine 的字母存到哈希表中。

代码

 var canConstruct = function(ransomNote, magazine) {
     const resSet = new Array(26).fill(0);
     const base = "a".charCodeAt(); // 将a的下标处理成0
     for(const i of magazine) {
         // index为当前字母所对应的下标
         let index = i.charCodeAt() - base;
         resSet[index]++;
     }
     // console.log(resSet)
     for(const i of ransomNote) {
         // index为当前字母所对应的下标
         let index = i.charCodeAt() - base;
         // 如果当前下标中元素为0
         if(!resSet[index]) return false
         resSet[index]--
     }
     return true
 };

关于代码的解读,可以参考我之前文章中对有效字母异位词那题的总结。链接: 【代码随想录 | day06】(JavaScript) 哈希表理论基础以及相关算法题 - 掘金 (juejin.cn)


15.三数之和

题目链接:15. 三数之和 - 力扣(LeetCode)

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请

你返回所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

这道题相对于两数之和,复杂之处就在于要去重。我们要求的是不能重复的三元组,但是三元组里面的元素是可以重复的。

  • a, b ,c, 对应的就是 nums[i],nums[left],nums[right]。
  • 考虑 a 的去重时,应该判断 nums[i] 与 nums[i-1] 是否相同。因为当 i 的下一个元素值和 i 相等时,不会被 pass 掉。不能有重复的三元组,但三元组内的元素是可以重复的!
  • 考虑 b 和 c 的去重时,为了避免取到结果[0, -1, 1]之后,left又遇到-1,right又遇到1。所以还需要判断一下。

代码

先排序,然后定义指针。考虑排序后的第一个数是否大于 0 。对 a 去重

 var threeSum = function(nums) {
     let result = [], len = nums.length;;
     // 先给数组排序
     nums.sort((a, b) => a-b)
     console.log(nums)
     
     for(let i = 0; i < len; i++) {
         // 双指针
         let left = i + 1, right = len - 1, a = nums[i];  
         
         // 排序后的第一个数大于0,则所以数都大于0
         if(a > 0) return result;
 ​
         // 去重
         if (a == nums[i - 1]) continue
 ​
         while(left < right) {
             let b = nums[left], c = nums[right];
             let sum = a + b + c;
 ​
             if (sum > 0) right--;
             else if (sum < 0) left++;
             else{
                 result.push([a, b, c])
                 // 去重
                 while(left < right && nums[left] == nums[left + 1]) {
                     left++;
                 }
                 while(left < right && nums[right] == nums[right - 1]) {
                     right--;
                 }
                 // 取到一个结果后,继续让指针移动
                 left++;
                 right--;
             }
         }
     }
 ​
     return result
 };

18.四数之和

题目链接:18. 四数之和 - 力扣(LeetCode)

给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):

0 <= a, b, c, d < n a、b、c 和 d 互不相同 nums[a] + nums[b] + nums[c] + nums[d] == target 你可以按 任意顺序 返回答案 。

四数相加相当于在三数相加的基础上再加一层for循环,但是有一点要注意的是。三数之和要求相加之和为 0 ,但是四数之和的相加目标值是不确定的。所以不用考虑排序后的第一个数是否大于 0。

思路:

  1. 考虑数组的特殊情况,然后对数组进行排序。
  2. 定义 i 和 j 以后,再使用两个指针 l 和 r

代码

 var fourSum = function(nums, target) {
     const len = nums.length;
     // 首先考虑数组长度小于4的情况
     if(len < 4) return [];
     
     nums.sort((a, b) => a - b);
     const res = [];
     
     for(let i = 0; i < len - 3; i++) {
         // 去重i
         if(i > 0 && nums[i] === nums[i - 1]) continue;
         for(let j = i + 1; j < len - 2; j++) {
             // 去重j
             if(j > i + 1 && nums[j] === nums[j - 1]) continue;
             let l = j + 1, r = len - 1;
             while(l < r) {
                 const sum = nums[i] + nums[j] + nums[l] + nums[r];
                 if(sum < target) { l++; continue}
                 if(sum > target) { r--; continue}
                 res.push([nums[i], nums[j], nums[l], nums[r]]);
                 while(l < r && nums[l] === nums[++l]);
                 while(l < r && nums[r] === nums[--r]);
             }
         } 
     }
     return res;
 };

注意点:

  • nums.sort((a, b) => a - b); 要不就写成左边这样,要不就是

     nums.sort((a, b) => {
       return a - b
      });
    

    千万不能写错!

  • f(sum < target) { l++; continue} 此时当 l++ 过后直接跳出本次while,而不是本次 for 循环!

  • 求sum一定是放在while(l < r) 之后的

  • 第一层for循环后,要去重 i ,第二层for循环要去重 j 。然后再去定义左右指针!

  • i++返回原来的值,++i返回加 1 后的值。二值执行顺序不同。

  • while(l < r && nums[r] === nums[--r]);就相当于while(l < r) && nums[r] === nums[r-1] r--;


参考文章