【西法帶你學算法】單調棧解題模板秒殺八道題

單調棧

顧名思義, 單調棧是一種棧。所以要學單調棧,首先要完全搞懂棧。git

棧是什麼?

棧是一種受限的數據結構, 體如今只容許新的內容從一個方向插入或刪除,這個方向咱們叫棧頂,而從其餘位置獲取內容是不被容許的github

棧最顯著的特徵就是 LIFO(Last In, First Out - 後進先出)算法

舉個例子:編程

棧就像是一個放書本的抽屜,進棧的操做就比如是想抽屜裏放一本書,新進去的書永遠在最上層,而退棧則至關於從裏往外拿書本,永遠是從最上層開始拿,因此拿出來的永遠是最後進去的哪個數組

棧的經常使用操做

  1. 進棧 - push - 將元素放置到棧頂
  2. 退棧 - pop - 將棧頂元素彈出
  3. 棧頂 - top - 獲得棧頂元素的值
  4. 是否空棧 - isEmpty - 判斷棧內是否有元素

棧的經常使用操做時間複雜度

因爲棧只在尾部操做就好了,咱們用數組進行模擬的話,能夠很容易達到 O(1)的時間複雜度。固然也能夠用鏈表實現,即鏈式棧。瀏覽器

  1. 進棧 - O(1)
  2. 出棧 - O(1)

應用

  • 函數調用棧
  • 瀏覽器前進後退
  • 匹配括號
  • 單調棧用來尋找下一個更大(更小)元素

題目推薦

單調棧又是什麼?

單調棧是一種特殊的棧。棧原本就是一種受限的數據結構了,單調棧在此基礎上又受限了一次(受限++)。數據結構

單調棧要求棧中的元素是單調遞增或者單調遞減的。app

是否嚴格遞增或遞減能夠根據實際狀況來。

這裏我用 [a,b,c] 表示一個棧。 其中 左側爲棧底,右側爲棧頂。編程語言

好比:函數

  • [1,2,3,4] 就是一個單調遞增棧
  • [3,2,1] 就是一個單調遞減棧
  • [1,3,2] 就不是一個合法的單調棧

那這個限制有什麼用呢?這個限制(特性)可以解決什麼用的問題呢?

適用場景

單調棧適合的題目是求解第一個一個大於 xxx或者第一個小於 xxx這種題目。全部當你有這種需求的時候,就應該想到單調棧。

那麼爲何單調棧適合求解第一個一個大於 xxx或者第一個小於 xxx這種題目?緣由很簡單,我這裏經過一個例子給你們講解一下。

這裏舉的例子是單調遞增棧

好比咱們須要依次將數組 [1,3,4,5,2,9,6] 壓入單調棧。

  1. 首先壓入 1,此時的棧爲:[1]
  2. 繼續壓入 3,此時的棧爲:[1,3]
  3. 繼續壓入 4,此時的棧爲:[1,3,4]
  4. 繼續壓入 5,此時的棧爲:[1,3,4,5]
  5. 若是繼續壓入 2,此時的棧爲:[1,3,4,5,2] 不知足單調遞增棧的特性, 所以須要調整。如何調整?因爲棧只有 pop 操做,所以咱們只好不斷 pop,直到知足單調遞增爲止。
  6. 上面其實咱們並無壓入 2,而是先 pop,pop 到壓入 2 依然能夠保持單調遞增再 壓入 2,此時的棧爲:[1,2]
  7. 繼續壓入 9,此時的棧爲:[1,2,9]
  8. 若是繼續壓入 6,則不知足單調遞增棧的特性, 咱們故技重施,不斷 pop,直到知足單調遞增爲止。此時的棧爲:[1,2,6]

注意這裏的棧仍然是非空的,若是有的題目須要用到全部數組的信息,那麼頗有可能因沒有考慮邊界而不能經過全部的測試用例。 這裏介紹一個技巧 - 哨兵法,這個技巧常常用在單調棧的算法中。

對於上面的例子,我能夠在原數組 [1,3,4,5,2,9,6] 的右側添加一個小於數組中最小值的項便可,好比 -1。此時的數組是 [1,3,4,5,2,9,6,-1]。這種技巧能夠簡化代碼邏輯,你們儘可能掌握。

上面的例子若是你明白了,就不難理解爲啥單調棧適合求解第一個一個大於 xxx或者第一個小於 xxx這種題目了。好比上面的例子,咱們就能夠很容易地求出在其以後第一個小於其自己的位置。好比 3 的索引是 1,小於 3 的第一個索引是 4,2 的索引 4,小於 2 的第一個索引是 0,可是其在 2 的索引 4 以後,所以不符合條件,也就是不存在在 2 以後第一個小於 2 自己的位置

上面的例子,咱們在第 6 步開始 pop,第一個被 pop 出來的是 5,所以 5 以後的第一個小於 5 的索引就是 4。同理被 pop 出來的 3,4,5 也都是 4。

若是用 ans 來表示在其以後第一個小於其自己的位置,ans[i] 表示 arr[i] 以後第一個小於 arr[i] 的位置, ans[i] 爲 -1 表示這樣的位置不存在,好比前文提到的 2。那麼此時的 ans 是 [-1,4,4,4,-1,-1,-1]。

第 8 步,咱們又開始 pop 了。此時 pop 出來的是 9,所以 9 以後第一個小於 9 的索引就是 6。

這個算法的過程用一句話總結就是,若是壓棧以後仍然能夠保持單調性,那麼直接壓。不然先彈出棧的元素,直到壓入以後能夠保持單調性。
這個算法的原理用一句話總結就是,被彈出的元素都是大於當前元素的,而且因爲棧是單調增的,所以在其以後小於其自己的最近的就是當前元素了

下面給你們推薦幾道題,你們趁着知識還在腦子來,趕忙去刷一下吧~

僞代碼

上面的算法能夠用以下的僞代碼表示,同時這是一個通用的算法模板,你們遇到單調棧的題目能夠直接套。

建議你們用本身熟悉的編程語言實現一遍,之後改改符號基本就能用。

class Solution:
    def monostoneStack(self, arr: List[int]) -> List[int]:
        stack = []
        ans = 定義一個長度和 arr 同樣長的數組,並初始化爲 -1
        循環 i in  arr:
            while stack and arr[i] > arr[棧頂元素]:
                peek = 彈出棧頂元素
                ans[peek] = i - peek
            stack.append(i)
        return ans

複雜度分析

  • 時間複雜度:因爲 arr 的元素最多隻會入棧,出棧一次,所以時間複雜度仍然是 $O(N)$,其中 N 爲數組長度。
  • 空間複雜度:因爲使用了棧, 而且棧的長度最大是和 arr 長度一致,所以空間複雜度是 $O(N)$,其中 N 爲數組長度。

代碼

這裏提升兩種編程語言的單調棧模板供你們參考。

Python3:

class Solution:
    def monostoneStack(self, T: List[int]) -> List[int]:
        stack = []
        ans = [0] * len(T)
        for i in range(len(T)):
            while stack and T[i] > T[stack[-1]]:
                peek = stack.pop(-1)
                ans[peek] = i - peek
            stack.append(i)
        return ans

JS:

var monostoneStack = function (T) {
  let stack = [];
  let result = [];
  for (let i = 0; i < T.length; i++) {
    result[i] = 0;
    while (stack.length > 0 && T[stack[stack.length - 1]] < T[i]) {
      let peek = stack.pop();
      result[peek] = i - peek;
    }
    stack.push(i);
  }
  return result;
};

題目推薦

下面幾個題幫助你理解單調棧, 並讓你明白何時能夠用單調棧進行算法優化。

總結

單調棧本質就是棧, 棧自己就是一種受限的數據結構。其受限指的是隻能在一端進行操做。而單調棧在棧的基礎上進一步受限,即要求棧中的元素始終保持單調性。

因爲棧中都是單調的,所以其天生適合解決在其以後第一個小於其自己的位置的題目。你們若是遇到題目須要找在其以後第一個小於其自己的位置的題目,就但是考慮使用單調棧。

單調棧的寫法相對比較固定,你們能夠本身參考個人僞代碼本身總結一份模板,之後直接套用能夠大大提升作題效率和容錯率。

我整理的 1000 多頁的電子書已經開放下載了,你們能夠去個人公衆號《力扣加加》後臺回覆電子書獲取。

你們對此有何見解,歡迎給我留言,我有時間都會一一查看回答。更多算法套路能夠訪問個人 LeetCode 題解倉庫:https://github.com/azl3979858... 。 目前已經 37K star 啦。

你們也能夠關注個人公衆號《力扣加加》帶你啃下算法這塊硬骨頭。

相關文章
相關標籤/搜索