每当我遇到一个难道,脑子里下意识出现的第一个想法就是干掉它。将复杂问题简单化,简单到不能再简单的地步,也是我文章一直追求的一点。面试
K Sum 求和问题这种题型套娃题,备受面试官喜好,经过层层拷问,来考察你对事物的观察能力和解决能力,这彷佛成为了每一个面试官的习惯和套路。战胜对手,首先要了解你的对手。算法
看似复杂的东西,背后其实就是简单的原理和机制,宇宙万物存在的事物亦是如此。深刻复杂问题内部,去看它简单的运行逻辑。将繁杂嵌套事物转化为可用简单动画表现的事物。这是一个由繁变简的过程,简单,简而不单,又单而不简。数组
诱饵:2Sum 两数之和ide
捕鱼,先要学会布网,看似一个简单题目,其实做为诱饵,引伸出背后的终极 Boss。性能
抛出诱饵:优化
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。动画
示例: 给定 nums = [2, 7, 11, 15], target = 9 由于 nums[0] + nums[1] = 2 + 7 = 9 因此返回 [0, 1]
大脑最早下意识想到的是,遍历全部数据,找出知足条件的这两个值,俗称暴力破解法。设计
解法一:暴力破解法指针
让目标值减去其中一个值,拿着差去数组中查找匹配是否存在,若是存在则返回下标。code
/** * 解法一:暴力破解 * @param {number[]} nums * @param {number} target * @return {number[]} */ var twoSum = function(nums, target) { // 判断数组为空的状况 if (nums == null || nums.length == 1) { return []; } for (let i = 0; i < nums.length; i++) { let item = nums[i]; if (nums.indexOf(target - item, i + 1) !== -1) { return [i, nums.indexOf(target - item, i + 1)]; } } return []; };
最坏的状况,须要两层 for 循环遍历全部状况。时间复杂度为 O(n²)。在这个过程当中,只须要常量大小的额外内存空间,空间复杂度为 O(1)。
上述解法,耗费时间太多,可是宇宙万物任何存在对立的两种事物都是能够互相转化,时间和空间也是如此。
解法二:哈希表
由于咱们在遍历的时候,用 target 取数据的时候,须要再遍历一遍数组,这才致使了耗费时间过长。咱们把这部分时间转化为空间,用空间换时间,能将空间换时间的非哈希表莫属。
数组原本就存在映射,经过下标取出对应值,可是咱们此次是经过 target 值减去其中一个值,获得另外一个值,经过这个值得出下标确不能。
因此须要让数组的值和下标索引作一层映射,若是已知值,能够经过哈希映射获得下标索引 index。
/** * 解法二:两遍哈希表法 * @param {number[]} nums * @param {number} target * @return {number[]} */ var twoSum2 = function(nums, target) { // 将值存储到哈希表中 let map = new Map(); // 存储 nums.forEach((item,index) => { map.set(item, index); }); // 判断 for (let i = 0; i < nums.length; i++) { let item = nums[i]; console.log(map.get(target - item)) if (map.has(target - item) && map.get(target - item) !== i) { return [i, map.get(target - item)]; } } return []; };
借助哈希表,空间换时间,时间效率下降了一个维度,时间复杂度为 O(n)。空间须要 n 大小的额外内存空间开辟哈希表的空间大小,空间复杂度为 O(n)。
解法三:哈希表优化
对于以上哈希表,咱们须要一遍先去存储值和索引的映射,若是咱们在遍历查找的时候存储,不是能够节省这个步骤吗?
若是咱们查找目标值的时候,在哈希表中查找,若是可以找到,就返回该值的下标,若是找不到,则将改值的映射加入到哈希表中,这样一边就完成查找数据和添加数据。
/** * 解法三:哈希表法优化 * @param {number[]} nums * @param {number} target * @return {number[]} */ var twoSum3 = function(nums, target) { // 将值存储到哈希表中 let map = new Map(); // 存储 for (let i = 0; i < nums.length; i++) { let item = nums[i]; if (map.has(target - item) && map.get(target - item) !== i) { return [map.get(target - item), i]; } map.set(item, i); } return []; };
上钩:3Sum 三数之和
此时,咱们的解答和优化受到面试官的表扬,你认为这完美的解题思路能够拿到 offer 的时候,但却这只是个热身,由于你已经上钩了。
上钩诱导:
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出全部知足条件且不重复的三元组。
例如:
给定数组 nums = [-1, 0, 1, 2, -1, -4], 知足要求的三元组集合为: [ [-1, 0, 1], [-1, -1, 2] ]
此次从两个数升级到了三个数,此时你内心的感觉是既高兴又担忧。咱们有了上一题的解题优化思路,你想着这道题会不会是一样的思路呢?
暴力破解三层 for 循环当然能解,可是确定耗费时间比 n² 还要长,因此你想着用哈希表作优化。
一、解法一:哈希表
先用两层 for 循环,固定两个数,而后在用哈希表去找第三个数,直到找到为止。
/** * 解法一:哈希表优化 * @param {number[]} nums * @return {number[][]} */ var threeSum = function(nums) { var res = []; var map = new Map(); for (let i = 0; i < nums.length - 2; i++) { for (let j = i + 1; j < nums.length - 1; j++) { let item = 0 - nums[i] - nums[j]; if (map.has(item)) { res.push([item, nums[i], nums[j]]); } else { map.set(item, 1); } } } return res; };
可是此次,并非空间换时间,由于两层 for 循环,致使了时间复杂度为 O(n²),并且空间上须要额外大小为 n 的内存空间存储哈希表,不只时间效率仍是空间效率,都是不乐观的。
此时的你,陷入了深思...
二、解法二:排序 + 双指针
以往的面试者到这里基本被淘汰掉了,剩下的为有经验的应聘者,他会根据以往的作题经验总结的来优化本题。
若是我先固定一个数,另外两个数我要懂得变通,若是三者和小于目标值,我再让另外两个数其中之一换一个大点的。若是三者之和大于目标值,我就让两个数其中一个换一个小点的。
对于换个大点的或者小的数据,一个数据要有阶梯的层次,就必须进行排序,排序最好的时间复杂度为 O(nlogn)。
咱们用两个指针,分别指向最大值和最小值,固定其中一个数,让这个固定的数和其他两个指针指向的数三者相加,若是小于目标值,就让指向最小值的数右移,变的大一些,不然,指向最大值的指针左移,指向的数稍微小一些。
/** * 解法二:排序 + 双指针(去重) * @param {number[]} nums * @return {number[][]} */ var threeSum = function(nums) { var res = []; var len = nums.length; // 判断特殊状况 if (nums == null || len < 3) return res; nums.sort((a, b) => a - b); // 从小到大排序 for (let i = 0; i < len; i++) { // 若是固定的数为正整数,不可能存在为 0 状况 if (nums[i] > 0) break; // 去重(若是下一个固定数和前一个相等,后边会出现重复结果) if (i > 0 && nums[i] == nums[i - 1]) continue; // 定义左右指针 let L = i + 1; let R = len - 1; while (L < R) { // 结束遍历条件 let sum = nums[i] + nums[L] + nums[R]; if (sum == 0) { // 去重 res.push([nums[i], nums[L], nums[R]]); while (L < R && nums[L] == nums[L + 1]) L++; while (L < R && nums[R] == nums[R - 1]) R--; L++; R--; } else if (sum < 0) { L++; } else if (sum > 0) { R--; } } return res; } };
若是你实践了,发现以前的解法也是行不通的,为啥?由于没有去重,好比[-1,0,1,-1]。其中[-1, 0, 1]、[0, 1, -1]结果都会让值等于目标值 0,可是这两个结果重复了。
要想作到去重,咱们就要找到去重的规律。我分为如下几个点:
num[i] > 0 时,不管左右指针如何移动,找不到任何知足条件的值。
// 若是固定的数为正整数,不可能存在为 0 状况 if (nums[i] > 0) break;
num[i] = num[i - 1] 当前值和前一个值重复,寻找的值也会重复,全部跳过。
// 去重(若是下一个固定数和前一个相等,后边会出现重复结果) if (i > 0 && nums[i] == nums[i - 1]) continue;
sum = 0 时,左右指针移动也会存在重复的值。
nums[L] = nums[L + 1],让 L++ 继续寻找下一个匹配的值。
while (L < R && nums[L] == nums[L + 1]) L++;
nums[R] = nums[R - 1],让 R-- 继续寻找下一个匹配的值。
和以上同理!
while (L < R && nums[R] == nums[R - 1]) R--;
经过上边对各个查重边界条件的判断,最后的结果不会有重复数据了。
在空间上,不须要空间大小为 n 的内存空间,空间复杂度降到 O(1)。
你觉得完事了,其实尚未,这才到了中期,也是在有经验的应聘者中筛选,面试官想要在最后的应聘者中再进行筛选,确定还要进一步考察你对本题的思考。
再来:4 Sum 四数求和
三数之和求解巧妙的排序设计和双指针的运用,已经让咱们对齐有些心有余力而力不足。
继续升级到 4 Sum 四数求和问题,若是有了以上的思路,4 Sum 求和难不到你,运用一样的思路,先固定两个数,而后仍是运用双指针求另外两个知足条件的数字。
/** * @param {number[]} nums * @param {number} target * @return {number[][]} */ var fourSum = function(nums, target) { var res = []; var len = nums.length; if (nums == null || len < 4) return res; nums.sort((a, b) => a - b); for (let i = 0; i < len - 3; i++) { // 去重(若是下一个固定数和前一个相等,后边会出现重复结果) if (i > 0 && nums[i] == nums[i - 1]) continue; //计算当前的最小值,若是最小值都比target大,不用再继续计算了 if (nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) break; //计算当前最大值,若是最大值都比target小,不用再继续计算了 if (nums[i] + nums[len - 1] + nums[len - 2] + nums[len - 3] < target) continue; // 肯定第二个指针的位置 for (let j = i + 1; j < len - 2; j++) { // 去重 if (j > i + 1 && nums[j] == nums[j - 1]) continue; // 定义第三/四个指针 let L = j + 1; let R = len - 1; //计算当前的最小值,若是最小值都比target大,不用再继续计算了 let min = nums[i] + nums[j] + nums[L] + nums[L + 1]; if (min > target) continue; //计算当前最大值,若是最大值都比target小,不用再继续计算了 let max = nums[i] + nums[j] + nums[R] + nums[R - 1]; if (max < target) continue; while (L < R) { let sum = nums[i] + nums[j] + nums[L] + nums[R]; if (sum == target) { res.push([nums[i], nums[j], nums[L], nums[R]]); } if (sum < target) { while (nums[L] === nums[++L]); } else { while (nums[R] === nums[--R]); } } } } return res; };
一样,对于 4 sum 四数求和的性能,借助排序 + 双指针方法并无使得效率和空间变坏,因此一样适用。
可是惟一不一样的就是一些特殊的边界条件变化,好比:
//计算当前的最小值,若是最小值都比target大,不用再继续计算了 if (nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) break; //计算当前最大值,若是最大值都比target小,不用再继续计算了 if (nums[i] + nums[len - 1] + nums[len - 2] + nums[len - 3] < target)
Boss:K Sum K 数求和
从 2Sum,上升到 3Sum,而后到了 4Sum,最后能够归结为 KSum 问题。
解题的关键不只仅是利用排序和双指针问题,并且对于几个特殊状况的优化,对总体d代码的执行效率也有很大关系。
在 4Sum 求和中,我尝试着增长了对几个特殊状况判断,如:
//计算当前的最小值,若是最小值都比target大,不用再继续计算了 let min = nums[i] + nums[j] + nums[L] + nums[L + 1]; if (min > target) continue; //计算当前最大值,若是最大值都比target小,不用再继续计算了 let max = nums[i] + nums[j] + nums[R] + nums[R - 1]; if (max < target) continue;
针对特殊状况优化的执行结果对好比下:
小结
经过对这个面试题深刻的分析和总结,收获不少,不只是对本题的收获,更多的是对全部算法题的一个归纳。
鹿哥,你这不仅是分析了一个算法题吗?你咋就对其余题目也有收获呢?
题不在于刷多,刷更多的题是为了熟悉更多的题型和练习本身对题目的敏感度或者能够说总结出算法题的一些套路。
这个题它自己就能够全部算法题从繁杂到简化的一个过程,跑不出空间与时间的转化,也跑不出对一些边界条件的思考,之后不管作什么算法题,都跑不出这两样东西。
咱们虽然看它表面在变化,可是它的实质并无变化,任何事物都由最简单的事物构成,所谓的复杂,只是你把它想象的过于复杂。