滑動窗口算法是較爲入門題目的算法,通常是一些有規律數組問題的最優解,也就是說,若是一個數組問題能夠用動態規劃解,但又可使用滑動窗口解決,那麼每每滑動窗口的效率更高。前端
雙指針也並不侷限在數組問題,像鏈表場景的 「快慢指針」 也屬於雙指針的場景,其快慢指針滑動過程當中自己就會產生一個窗口,好比當窗口收縮到某種程度,能夠獲得一些結論。git
所以掌握滑動窗口很是基礎且重要,接下來按照個人經驗給你們介紹這個算法。github
滑動窗口使用雙指針解決問題,因此通常也叫雙指針算法,由於兩個指針間造成一個窗口。算法
什麼狀況適合用雙指針呢?通常雙指針是暴力算法的優化版,因此:數組
也就是說,當一個問題比較有規律,或者較爲簡單,或較爲巧妙時,能夠嘗試雙指針(滑動窗口)解法。微信
咱們仍是拿例子說明,首先是兩數之和。優化
兩數之和是一道簡單題,實際上和滑動窗口沒什麼關係,但爲了引出三數之和,仍是先講這道題。題目以下:指針
給定一個整數數組
nums
和一個整數目標值target
,請你在該數組中找出 和爲目標值target
的那 兩個 整數,並返回它們的數組下標。code你能夠假設每種輸入只會對應一個答案。可是,數組中同一個元素在答案裏不能重複出現。cdn
暴力解法就是窮舉全部兩數之和,發現和爲 target
結束,顯然這種作法有點慢,咱們換一種思路。
因爲能夠用空間換時間,又只有兩個數,咱們能夠對題目進行轉化,即經過一次遍歷,將 nums
每一項都減去 target
,而後找到後面任意一項值爲前面的結果,即表示它們和爲 target
。
能夠用哈希表 map
加速查詢,即將每一項 target - num
做爲 key,若是後面任何一個 num
做爲 key 能夠在 map
中找到,則得解,且上一個數的原始值能夠存在 map
的 value 中。這要僅需遍歷一次,時間複雜度爲 O(n)。
之因此說這道題,是由於這道題是單指針,即只有一個指針在數組中移動,並配合哈希錶快速求解。對於稍微複雜的問題,單指針就不夠了,須要用雙指針解決(通常來講不會用到三或以上指針),那複雜點的題目就是三數之和了。
三數之和是一道中等題,別覺得只是兩數之和的增強版,其思路徹底不一樣。題目以下:
給你一個包含n
個整數的數組nums
,判斷nums
中是否存在三個元素a
,b
,c
,使得a + b + c = 0
?請你找出全部和爲0
且不重複的三元組。
因爲超過了兩個數,因此不能像雙指針同樣求解了,由於即使用了哈希表存儲,也會在遍歷時遇到 「兩數之和」 的問題,而哈希表方案沒法繼續嵌套使用,即沒法進一步下降複雜度。
爲了下降時間複雜度,咱們但願只遍歷一次數組,這就須要數組知足必定條件咱們才能用滑動窗口,因此咱們對數組進行排序,使用快排的時間複雜度爲 O(nlogn),時間複雜度已超出兩數之和,不過由於題目複雜,這個犧牲是沒法避免的。
假設從小到大排序,那咱們就拿到一個遞增數組了,此時經典滑動窗口方法就可用了!怎麼滑動呢?首先建立兩個指針,分別叫 left
與 right
,經過不斷修改 left
與 right
,讓它們在數組間滑動,這個窗口大小就是符合題目要求的,當滑動完畢時,返回全部知足條件的窗口便可,記錄其實很簡單,只要在滑動過程當中記錄一下就行。
首先排除異常值,即數組長度太小,而後對於常規狀況,咱們拿一個全局變量存儲當前窗口數的和,這樣 right + 1
只要累加 nums[right+1]
,left + 1
只要減去 nums[left]
便可快速拿到求和。
因爲須要考慮全部狀況,因此須要一次數組遍歷,對於每次遍歷的起始點 i
,若是 nums[i] > 0
則直接跳過,由於數組排序後是遞增的,後面的和只會永遠大於 0;不然進行窗口滑動,先造成三個點 [i, i+1, n-1]
,這樣保持 i
不動,不斷包夾後兩個數字便可,只要它們的和大於 0,就將第三個點左移(數字會變小),不然將第二個點右移(數字會變大),其實第二個和第三個數就是滑動窗口。
這樣的話時間複雜度是 O(n²),由於存在兩次遍歷,忽略快排較小的時間複雜度。
那麼四數之和,五數之和呢?
該題和三數之和徹底同樣,除了要求變成四個數。
首先仍是排序,而後雙重遞歸,即肯定前兩個數不變,不斷包夾後兩個數,後兩個數就是 i+1
和 n-1
,算法和三數之和同樣,因此最終時間複雜度爲 O(n³)。
那麼 N 數之和(N > 2)均可以採用這個思路解決。
爲何沒有更優的方法呢?我想可能由於:
因此對於 N 數之和,經過排序付出了 O(nlogn) 時間複雜度以後,能夠用滑動窗口,將 2 個數時間複雜度優化爲 O(n),因此總體時間複雜度就是 O(N - 2 + 1 個 n),即 O(N-1 個 n),而最小的時間複雜度 O(n²) 比 O(nlogn) 大,因此老是忽略快排的時間複雜度,因此三數之和時間複雜度是 O(n²),四數之和時間複雜度爲 O(n³),依此類推。
能夠看到,咱們從最簡單的兩數之和,到三數之和、四數之和,跨入了滑動窗口的門檻,本質上是利用排序後數組有序的特性,讓咱們在不用遍歷數組的前提下,能夠對窗口進行滑動,這是滑動窗口算法的核心思想。
爲了增強這個理解,再看一道相似的題目,無重複字符的最長子串。
無重複字符的最長子串是一道中等題,題目以下:
給定一個字符串,請你找出其中不含有重複字符的 最長子串 的長度。
因爲最長子串是連續的,因此顯然能夠考慮滑動窗口解法。其實肯定了滑動窗口解法後,問題很簡單,只要設定 left
和 right
,並用一個哈希 Set 記錄哪些元素存在過,在過程當中記錄最大長度,並嘗試 right
右移,若是右移過程當中發現出現重複字符,則 left
右移,直到消除這個重複字符爲止。
解法並不難,但問題是,咱們要想清楚,爲何用滑動窗口遍歷一次就能夠作到 不重不漏?即這道題時間複雜度只有 O(n) 呢?
只要想明白兩個問題:
right
右移,且出現重複後嘗試將 left
右移到不重複後,right
再繼續右移,這忽略了出現重複後, right
左移的狀況。咱們重點看二個問題,顯然,若是 abcd
這四個連續的字符不重複,那麼 left
右移後,bcd
也顯然不重複,因此若是此時就能夠將 right
右移造成 bcda
的窗口繼續找下去,而不須要嘗試 bc
這種狀況,由於這種狀況雖然不重複,但必定不是最優解。
好了,經過這個例子咱們看到,滑動窗口如何縮小窗口範圍其實不難,但更要注重的是,背後對於爲何能夠用滑動窗口的思考,滑動窗口有沒有作到不重不漏,若是沒有想清楚,可能整個思路都錯了。
那麼滑動窗口的應用已經說透了?其實沒有,咱們上面只說了縮小窗口這種比較單一的腦回路,其實雙指針構成的滑動窗口不必定都是那麼正常滑的,一種有意思的場景是快慢指針,便是以相對速度決定窗口如何滑動。
關於快慢指針,經典的題目有環形鏈表、刪除有序數組中的重複項。
環形鏈表是一道簡單題,題目以下:
給定一個鏈表,判斷鏈表中是否有環。
若是不是進階要求空間複雜度 O(1),咱們能夠在遍歷時稍稍 「污染」 一下原始鏈表,這樣總能發現是否走了回頭路。
但要求空間開銷必須是常數,咱們不得不考慮快慢指針。說實話第一次看到這道題時,若是能想到快慢指針的解法,絕對是至關聰明的,由於必需要有知識遷移的能力。怎麼遷移呢?想象學校在開運動會,相信每次都有一個跑的最慢的同窗,慢到被最快的同窗追了一圈。
等等,操場不就是環形鏈表嗎?只要有人跑得慢,就會被跑得快的追上,追上不就是相遇了嗎? 因此快慢指針分別跑,只要相遇則斷定爲環形鏈表,不然不是環形鏈表,且必定有一個指針先走完。
那麼細枝末節就是優化效率了,慢指針到底慢多少呢?
有人會說,運動會上,跑步慢的人若是想被快的人追上,最好就不要跑。對,但環形鏈表問題中,鏈表不是操場,可能只有某一段是環,也就是跑步慢的人至少要跑到環裏,纔可能與跑得快人的相遇,但跑得慢的人又不知道哪裏開始成環,這就是難點。
你有沒有想過,爲何快排用二分法,而不是三分法?爲何每次中間來一刀,能夠最快排完?緣由是二分能夠用最小的 「深度」 將數組切割爲最小粒度。那麼同理,快慢指針中,慢指針要想被儘快追上,速度可能最好是快指針的一半。那從邏輯上分析,爲何呢?
直觀來看,若是慢指針太慢,可能大部分時間都在進入環形以前的位置轉悠,快指針雖然快,但永遠在環裏跑,因此老是沒法遇到慢指針,這給咱們的啓示是,慢指針不能太慢;若是慢指針太快,幾乎速度和快指針同樣,就像兩個運動員都各執己見的爭奪第一同樣,他們真的想相遇,估計得連續跑幾個小時吧,因此慢指針也不能過快。因此這樣分析下來,慢指針只能取折中的一半速度。
但用一半的慢速真的能最快相遇嗎?不必定,舉一個例子,假設鏈表是完美環形,一共有 [1,6] 共 6 個節點,那麼慢指針一次走 1 步,快指針一次走 2 步,那麼一共是 2,3 3,5 4,1 5,3 6,5 1,1
共走 6 步,但若是快指針一次走 3 步呢?一共是 2,4 3,1 4,4
3 步。這麼說通常速度不必定最優?其實不是的,計算機在鏈表尋址時,節點訪問的消耗也要考慮進去,後者雖然看上去更快,但其實訪問鏈表 next
的次數更多,對計算機來講,還不如第一種來得快。
因此準確來講,不是快指針比慢指針快一倍速度,而是慢指針一次走一步,快指針一次走兩步最優,由於相遇時,總移動步數最少。
再說一個簡單問題,即用快慢指針判斷鏈表中倒數第k個節點或者鏈表中點。
快指針是慢指針速度 2 倍,當快指針到達尾部,慢指針的位置就是鏈表中點。
鏈表中倒數第k個節點是一道簡單題,題目以下:
輸入一個鏈表,輸出該鏈表中倒數第k
個節點。爲了符合大多數人的習慣,本題從1
開始計數,即鏈表的尾節點是倒數第1
個節點。
這道題就是判斷鏈表中點的變種,只要讓慢指針比快指針慢 k
個節點,當快指針到達末尾時,慢指針就指向倒數第 k+1
個節點了。這道題注意一下數數別數錯了便可。
接下來終於說道快慢指針的另外一種經典用法題型,刪除有序數組中的重複項了。
刪除有序數組中的重複項是一道簡單題,題目以下:
給你一個有序數組
nums
,請你
原地 刪除重複出現的元素,使每一個元素 只出現一次 ,返回刪除後數組的新長度。
這道題,要原地刪除重複元素,並返回長度,因此只能用快慢指針。但怎麼用呢?快多少慢多少?
其實這道題快多少慢多少並不像前面題目同樣預設好了,而是根據遇到的實際數字來判斷。
咱們假設慢指針是 slow
快指針是 fast
,注意變量命名也有意思,一樣是雙指針問題,有的是 slow right
,有的是 slow fast
,重點在於用何種方法移動指針。
咱們只要讓 fast
掃描徹底表,把全部不重複的挪到一塊兒就行了,這樣時間複雜度是 O(n),具體作法是:
slow
和 fast
初始都指向 index 0。fast
直接日後掃描,只有遇到和 slow
不一樣的值,才把其和 slow+1
交換,而後 slow
自增,繼續遞歸,直到 fast
走到數組尾部結束。作完這套操做後,slow
的下標值就是答案。
能夠看到,這道題對於慢指針要如何慢,實際上是根據值來判斷的,若是 fast
的值與 slow
同樣,那麼 slow
就一直等着,由於相同的值要被忽略掉,讓 fast
走就是在跳太重複值。
說完了常見的雙指針用法,咱們再來看一些比較難啃的特殊問題,這裏主要講兩個,分別是 盛最多水的容器 與 接雨水。
盛最多水的容器是一道中等題,題目以下:
給你n
個非負整數a1,a2,...,an
,每一個數表明座標中的一個點(i, ai)
。在座標內畫n
條垂直線,垂直線i
的兩個端點分別爲(i, ai)
和(i, 0)
。找出其中的兩條線,使得它們與x
軸共同構成的容器能夠容納最多的水。
<img width=400 src="https://z3.ax1x.com/2021/06/12/25WZZt.png">
建議先仔細讀一讀題目再繼續,這道題相對比較複雜。
好了,爲何說這是一道雙指針題目呢?由於咱們看怎麼計算容納水的體積?其實這道題就簡化爲長乘寬。
長度就是選取的兩個柱子的間距,寬就是其中最短柱子的高度。問題就是,雖然柱子間距越遠,長度越大,但寬度不必定最大,一眼是無法看出來最優解的。
因此仍是得屢次嘗試,那怎麼樣能夠用最少的嘗試次數,但又不重不漏呢?定義 left
right
兩個指針,分別指向 0
與 n-1
即首尾兩個位置,此時長度是最大的(柱子間距離是最遠的),接下來嘗試一下別的柱子,試哪一個呢?
因此咱們移動較短的那個,並每次計算一下體積,最後當兩根柱子相遇時結束,過程當中最大致積就是全局最大致積。
這道題雙指針的移動規則比較巧妙,與上面普通題目不同,重點不是在是否會運用滑動窗口算法,而是可否找到移動指針的規則。
固然你可能會說,爲何兩個指針要定義在最兩端,而非別的地方?由於這樣就沒法控制變量了。
若是指針選在中間位置,那麼指針外移時,柱子的間距與柱子長度同時變化,就很難找到一條完美路線。好比咱們移動較短的柱子,是由於較短的柱子肯定了最低水位,改變它,可能讓最低水位變高,但問題是兩根柱子的間距也在變大,這樣移動較短仍是較長的柱子哪一個更優就說不許了。
說實話這種方法不太容易想到,須要多找幾種選擇嘗試才能發現。固然,算法若是按照固定套路就能推導出來,也就沒有難度了,因此要接受這種思惟跳躍。
接下來咱們看一道更特殊的滑動窗口問題,接雨水,它甚至分爲多段滑動窗口。
接雨水是一道困難題,題目以下:
給定n
個非負整數表示每一個寬度爲1
的柱子的高度圖,計算按此排列的柱子,下雨以後能接多少雨水。
<img width=400 src="https://z3.ax1x.com/2021/06/12/25OejP.png">
與盛雨水不一樣,這道接雨水看的是總體,咱們要算出能接的全部水的數量。
其實相比上一道題,這道題還算比較好切入,由於咱們從左到右計算便可。思考發現,只有產生了 「凹槽」 才能接到雨水,而凹槽由它兩邊最高的柱子決定,那什麼範圍算一段凹槽呢?
顯然凹槽是能夠明確分組的,一個凹槽也沒法被分割爲多個凹槽,就像你看水坑同樣,不管有多少,多深的坑在一塊兒,總能一個一個數清楚,因此咱們就從左到右開始數。
怎麼數凹槽呢?用滑動窗口辦法,每一個窗口就是一個凹槽,那麼窗口的起點 left
就是左邊第一根柱子,有如下狀況:
left++
。若是直接相鄰的右邊柱子更矮,那就有產生凹槽的機會。
若是右邊出現一個高一些的,就能夠接到雨水,那問題是怎麼算能接多少,以及找到哪結束呢?
這道題,一旦遇到凹槽結束點,left
就會更新,開始新的一輪凹槽計算,因此存在多個滑動窗口。從這道題能夠看出,滑動窗口題型至關靈活,不只判斷條件因題而異,窗口數量可能也有多個。
滑動窗口本質是雙指針的玩法,不一樣題目有不一樣的套路,從最簡單的按照規律包夾,到快慢指針,再到無固定套路的因題而異的特殊算法。
其實按照規律包夾的套路屬於碰撞指針範疇,通常對於排序好的數組,能夠一步一步判斷,或者用二分法判斷,總之不用根據總體遍從來判斷,效率天然高。
快慢指針也有套路可循,但具體快多少,或者慢多少,可能具體場景要具體看。
對於無固定套路的滑動窗口,就要根據題目仔細品味啦,若是全部套路都能總結出來,算法也少了樂趣。
討論地址是: 精讀《算法 - 滑動窗口》· Issue #328 · dt-fe/weekly
若是你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公衆號
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
版權聲明:自由轉載-非商用-非衍生-保持署名( 創意共享 3.0 許可證)