开启掘金成长之旅!这是我参与「掘金日新计划 · 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.赎金信
注意:
magazine
中的每个字符只能在ransomNote
中使用一次ransomNote
和magazine
由小写英文字母组成
这道题目和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.三数之和
给你一个整数数组 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.四数之和
给你一个由 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。
思路:
- 考虑数组的特殊情况,然后对数组进行排序。
- 定义 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--;