Appearance
Map 的妙用——两数求和问题
真题描述: 给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目 标值的那 两个 整数,并返回他们的数组下标。
给定 nums = [2, 7, 11, 15], target = 9 因为 nums[0] + nums[1] = 2 + 7 = 9 所以返 回 [0, 1]
最淳朴的解法:两层循环来遍历同一个数组;第一层循环遍历的值记为 a,第二层循环时遍 历的值记为 b;若 a+b = 目标值,那么 a 和 b 对应的数组下标就是我们想要的答案。但 是这样做的时间复杂度是:O(n^2)
记住一个结论:几乎所有的求和问题,都可以转化为求差问题。 这道题就是一个典型 的例子,通过把求和问题转化为求差问题,事情会变得更加简单。
思路:使用 Map 去存储已经遍历过的数字及其对应的索引值,map 的 key 是遍历的数字 ,值是索引。
判断依据:差值就是 taget 值减去遍历的数字,如果差值在 Map 记 录中存在,那么答案就出来了,就是当前的数字以及 Map 记录中 key 为差值的那一项
使用 for 循环实现
const twoSum = (nums, target)=> {
// 这里我用对象来模拟 map 的能力
const diffs = {}
// 缓存数组长度
const len = nums.length
// 遍历数组
for(let i=0;i<len;i++) {
// 判断 target 值减去当前遍历数字是否存在于 Map 中,存在则找到答案啦
if(diffs[target-nums[i]] !== undefined) {
// 若存在,返回这个差值对应的 Map key值的索引,以及当前索引
return [diffs[target - nums[i]], i]
}
// 若不存在,则记录当前值
diffs[nums[i]]=i
}
};
使用 map 实现
const twoSum = (nums, target)=> {
// 这里我用对象来模拟 map 的能力
const diffs = new Map()
// 缓存数组长度
const len = nums.length
for(let i=0;i<len;i++) {
console.log(diffs);
// 判断 target 值减去当前遍历数字是否存在于 Map 中,存在则找到答案啦
if(diffs.get(target - nums[i]) !== undefined) {
// 若存在,返回这个差值对应的 Map key值的索引,以及当前索引
return [diffs.get(target - nums[i]), i]
}
// 若不存在,则记录当前值
diffs.set(nums[i],i)
}
};
强大的双指针法(数组一定要有序)
合并两个有序数组:给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
输入:
nums1 = [2,3,6,7,8], m = 5
nums2 = [3,4,5], n = 3
输出: [1,3,3,4,5,6,7,8]
思路:
- 将 nums1 的长度变成两数组之和,即:nums1 = [2,3,6,7,8,0,0,0],nums1 尾部索引 为 k = m + n -1
- 定义两个指针,head 分别指向两个数组的尾部, 分别为 m - 1,n - 1
- 当两个指针都大于 0 时,比较对应值的大小,较大的那一方指针向前移动一位,nums1 的指针也向前移动一位,并且 nums1[k] = 较大值
- 遍历到最后,如果 m > n,因为我们初始化的时候就是拿的 nums1,所以不需要做任何操 作;如果 m < n,那么我们把 nums2 后面那几个没有比较的数据插入到 nums1 即可
const merge = function(nums1, m, nums2, n) {
// 初始化两个指针的指向,初始化 nums1 尾部索引k
let i = m - 1, j = n - 1, k = m + n - 1
// 当两个数组都没遍历完时,较大那一方的指针向前移动一位,并且 nums1 的指针 k 也向前移动一位
while(i >= 0 && j >= 0) {
// 取较大的值,从末尾往前填补
if(nums1[i] >= nums2[j]) {
nums1[k] = nums1[i]
i--
} else {
nums1[k] = nums2[j]
j--
}
k--
}
// nums2 留下的情况,特殊处理一下,把 nums2 后面那几个没有比较的数据插入到 nums1
while(j>=0) {
nums1[k] = nums2[j]
k--
j--
}
return nums1
};
console.log(merge([2,3,6,7,8],5,[3,4,5],3)); // [1,3,3,4,5,6,7,8]
三数求和问题(对撞指针法)
真题描述:给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
给定数组 nums = [-1, 0, 1, 2, -1, -4], 满足要求的三元组集合为: [ [-1, 0, 1], [-1, -1, 2] ]
思路:
- 双指针法只适用有序的数组,通过 sort 对数组先进行排序。定义结果数组 res = []
- 循环遍历数组,每次固定当前遍历值 nums[i],定义两个指针分别位于固定值后面的首尾 :j = i+1(首),k = arr.length - 1。明白 k 肯定大于 j 的,如果遇到相同数据直接 跳过 continue
- 遍历时,在满足 j < k 的条件下:判断 nums[i] + nums[j] + nums[k] = 0。如果大于 0,说明 k 偏大,需要左移,即 k--;如果小于 0,说明 j 偏小,需要右移 即 j++。同 时如果遇到相同的数据则直接跳过进行移动操作。
- 在遍历过程中,遇到满足条件的将 res.push([nums[i],nums[j],nums[k]]),并且两个指 针都要移动,并且判断是否遇到相同数据情况
const threeSum = function(nums) {
// 用于存放结果数组
let res = []
// 给 nums 排序
nums = nums.sort((a,b)=>{
return a-b
})
// 缓存数组长度
const len = nums.length
// 注意我们遍历到倒数第三个数就足够了,因为左右指针会遍历后面两个数
for(let i=0;i<len-2;i++) {
// 左指针 j
let j=i+1
// 右指针k
let k=len-1
// 如果遇到重复的数字,则跳过
if(i>0&&nums[i]===nums[i-1]) {
continue
}
while(j<k) {
// 三数之和小于0,左指针前进
if(nums[i]+nums[j]+nums[k]<0){
j++
// 处理左指针元素重复的情况
while(j<k&&nums[j]===nums[j-1]) {
j++
}
} else if(nums[i]+nums[j]+nums[k]>0){
// 三数之和大于0,右指针后退
k--
// 处理右指针元素重复的情况
while(j<k&&nums[k]===nums[k+1]) {
k--
}
} else {
// 得到目标数字组合,推入结果数组
res.push([nums[i],nums[j],nums[k]])
// 左右指针一起前进
j++
k--
// 若左指针元素重复,跳过
while(j<k&&nums[j]===nums[j-1]) {
j++
}
// 若右指针元素重复,跳过
while(j<k&&nums[k]===nums[k+1]) {
k--
}
}
}
}
// 返回结果数组
return res
};
总结
学到了两个方法:Map 保存遍历值(求和转求差)、双指针法(同位置移动、固定值+对撞 指针)仅针对有序数组
当遇到求和的问题,要转换成求差,通过 Map 去保存遍历数据,达到降低时间复杂度的效 果
当遇到 “有序”和“数组”:立刻把双指针法调度进你的大脑内存。普通双指针走不通,立刻 想对撞指针!
即便数组题目中并没有直接给出“有序”这个关键条件,我们在发觉普通思路走不下去的时候 ,也应该及时地尝试手动对其进行排序试试看有没有新的切入点——没有条件,创造条件也要 上。
指针法好像就是,在满足条件的情况下,移动的时候比较大小,然后选择移动的目标或者方 向,判断条件