前綴和的思路在力扣不少題目的都出現過,經常使用於處理連續子數組類型的的問題。接下來將用逐層深刻的方式來進行介紹,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則是額外的細節,沒有一開始就直接介紹最終的算法,保留中間的軌跡是爲了能更方便你們理解。
慣例:若是內容有錯誤的地方歡迎指出(以爲看着不理解不舒服想吐槽也徹底沒問題);若是有幫助,歡迎點贊和收藏,轉載請徵得贊成後著明出處,若是有問題也歡迎私信交流,主頁有郵箱地址
最後順便打個小廣告: