前缀和的思路在力扣不少题目的都出现过,经常使用于处理连续子数组类型的的问题。接下来将用逐层深刻的方式来进行介绍,javascript
看一道例题(leetcode 560)。java
给定一个整数数组和一个整数 k,你须要找到该数组中和为 k 的连续的子数组的个数。示例 1 :
输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不一样的状况。es6说明 :
数组的长度为 [1, 20,000]。
数组中元素的范围是 [-1000, 1000] ,且整数 k 的范围是 [-1e7, 1e7]。面试
首先看到这道题,最容易想到的是暴力破解:求出全部连续子数组的和,而后遍历它们而且统计其中和为k的项数。算法
为了方便 咱们定义一个sum
函数 用于求任意连续子数组的和,代码以下:数组
// 思路1:求出全部连续子数组的和 并统计知足和为k的项数 var subarraySum = function(nums, k) { const len = nums.length; let count = 0; for(let i = 0; i < len; i++){ for(let j = i+1; j < len; j++){ if(sum(nums,i,j)===k){ count++ } } } return count; }; // 求数组中从下标startIndex到下标endIndex之间全部元素的和 function sum(arr, startIndex, endIndex){ let res =0; for(let i = startIndex; i<=endIndex ;i++){ res += arr[i]; } return res; }
这种解法的时间复杂度显然过高了:最外层外面有两层循环 复杂度为O(n^2), sum(arr, i, j)的复杂度为n,因此总的时间复杂度达到了O(n^3)。所以咱们要考虑其余的思路。缓存
首先咱们先想办法优化掉sum
函数,由于这个函数对任何一个连续子数组都从新计算一次元素之和,不能有效利用以前的结果。 函数
所以咱们引入一个preSum
数组,其中preSum[i]表
示从数组从开始到下标为i
的全部元素的和.也就是:优化
var getPreSum = function(nums){ let count = 0; const preSum = [];// preSum【i】 表示从开始到第i个元素之和 for(let i = 0; i < nums.length; i++){ if(i === 0){ preSum[i] = num[i]; } else{ preSum[i] = preSum[i-1] + num[i] } } preSum[-1] = 0; // 因为数组从第0项开始,因此preSum[-1]表示没有元素 天然是为0. 这是为了方便后面的求解 return preSum; } getPreSum([1,1,1]); // 如今对于题目中输入的[1, 1, 1]数组 咱们就获得了一个值为[1,2,3] 的preSum数组
会发现,获得preSum
这个数组以后,求解第i
项到第j
项的元素之和, 等价于求preSum[j] - preSum[i-1]
。 (i=0
时,i-1= -1
,这也是上面设置preSum[-1] =0
的缘由)code
因此咱们如今成功把sum
函数去掉了,因为preSum[j+1]- preSum[i]
的复杂度是O(1),因此总体的复杂度也从O(n^3)下降到O(n^2)。使用前缀和以后,咱们的代码变成如今这样:
var subarraySum = function(nums, k) { const len = nums.length; let count = 0; const preSum = getPreSum(nums); // 请注意这里的i jd的范围和边界条件, 当i = j时, preSum[j] - preSum[i-1] = num[i] for(let i = 0; i < len; i++){ for(let j = i; j < len; j++){ if(preSum[j] - preSum[i-1] === k){ count++; } } } return count; };
可是这样的复杂度O(n^2)依然是不够的,因此咱们如今继续优化,目前主要的复杂度集中在嵌套的for
循环里,因此先观察下这个循环:
内层循环的关键条件语句是preSum[j] - preSum[i-1] === k
,根据等式的基本原理,移项可得 preSum[i-1] === preSum[j] - k
,
如今关键点来了,从j
的角度考虑(要考虑任意的i<=j
这就是前面提醒读者注意边界条件的缘由):
preSum[-1]
是否等于 preSum[0] - k
;preSum[0]
是否等于 preSum[1] - k
, preSum[-1]
是否等于 preSum[1] - k
;preSum[1]
是否等于 preSum[2] - k
,preSum[0]
是否等于 preSum[2] - k
, preSum[-1]
是否等于 preSum[2] - k
;...
发现了吗?在上述过程当中,其实咱们屡次用到了preSum[0]
, preSum[1]
, ... preSum[len]
因此若是咱们直接把preSum[i]
(0<=i<=len-1)缓存起来,就能够解开内层循环了. 因此咱们能够考虑用一个hash
结构(在js里面一般用obj或者es6里的map)来保存preSum[i] - k
, key => value
分别对应 preSum[i] - k => 出现次数
。
因为咱们预设了preSum[-1] = 0,因此hash结构的第一项默认就是, 0=>1 表示前缀和为0的状况已经出现了一次
接下来,遍历数组中的每一项,而且执行:
hash
,是否存在知足hash[已有的前缀和] - k
等于【当前的前缀和】hash
,把当前的前缀和添加到hash
中去 -- 若是已经存在hash[当前前缀和],那么出现次数加1; 若是还不存在,那出现次数设置为1;因此咱们能够把上面的思路用代码表示出来:
var subarraySum = function(nums, k) { const len = nums.length; let count = 0; const hash = new Map(); hash.set(0,1); //预设了preSum[-1]= 0; const preSum = getPreSum(nums); for(let i = 0; i < len; i++){ // 操做1: 判断以前出现的前缀和中 是否已经有知足【当前前缀和】=【以前前缀和】- k的项 const key = preSum[i] - k; if(hash.has(key){ count += hash.get(key); } // 操做2:把当前项对应的前缀和放入hash, 这个和上面的操做1的执行顺序是不能够相反的,不然会出现重复计数的问题 能够思考下为何 if(!hash.has(preSum[i])){ hash.set(preSum[i], 1); } else { hash.set(preSum[i], hash.get(preSum[i]) + 1); } } return count; };
通过第二步骤的优化以后,其实已经获得了一个比较好的前缀和算法,只有一层循环,因此时间复杂度为O(n),须要一个preSum的数组空间和一个map,空间复杂度为O(n)。不过仍是有地方能够优化: preSum
必须存在吗?
答案是没有必要的,咱们发现getPreSum
的本质实质上也是对nums
作了一次单层循环,而且在subarraySum
函数里, 遍历到i
时,咱们只须要当前对应的preSum[i]便可**
因此能够改写成如下形式:
var subarraySum = function(nums, k) { const len = nums.length; let count = 0; const hash = new Map(); hash.set(0,1); //预设了preSum[-1]= 0; // const preSum = getPreSum(nums); //这一行再也不须要了 用一个临时变量代替 let currentSum = 0; // 这个初始值其实对应的就是原先的preSum[-1] for(let i = 0; i < len; i++){ currentSum += num[i]; //这一步就求解了preSum[i] // 操做1: 判断以前出现的前缀和中 是否已经有知足【当前前缀和】=【以前前缀和】- k的项 const key = currentSum - k; if(hash.has(key){ count += hash.get(key); } // 操做2:把当前项对应的前缀和放入hash, 这个和上面的操做1的执行顺序是不能够相反的,不然会出现重复计数的问题 能够思考下为何 if(!hash.has(preSum[i])){ hash.set(preSum[i], 1); } else { hash.set(preSum[i], hash.get(preSum[i]) + 1); } } return count; };
到这里基本上完整的算法就介绍完了。
示例:
输入:A = [4,5,0,-2,-3,1], K = 5
输出:7
解释:
有 7 个子数组知足其元素之和可被 K = 5 整除:
[4, 5, 0, -2, -3, 1], [5], [5, 0], [5, 0, -2, -3], [0], [0, -2, -3], [-2, -3]
学完本文以后有兴趣的能够在leetcode上拿这道相似的题目练练手。
本文针对前缀和算法,以leetcode的一道题为例,按照由浅入深的方式,层层递进地进行介绍。
思路1是咱们接触算法较少时最多见最直接的解法;优化1是引入前缀和概念,优化2是保证前缀和时间复杂度知足通常要求的关键步骤,初次理解有难度;优化3则是额外的细节,没有一开始就直接介绍最终的算法,保留中间的轨迹是为了能更方便你们理解。
惯例:若是内容有错误的地方欢迎指出(以为看着不理解不舒服想吐槽也彻底没问题);若是有帮助,欢迎点赞和收藏,转载请征得赞成后著明出处,若是有问题也欢迎私信交流,主页有邮箱地址
最后顺便打个小广告: