「面向 offer 學算法」筆面試大殺器 -- 單調棧

目錄

  1. 前言
  2. 單調棧
  3. 初入茅廬
  4. 小試牛刀
  5. 打怪升級
  6. 出師試煉

前言

單調棧是一種比較簡單的數據結構。雖然簡單,但在某些題目中能發揮很好的做用。java

最近不少大廠的筆試、面試中都出現了單調棧的題目,而還有很多小夥伴連單調棧是什麼都不瞭解,所以老汪專門寫了這篇文章,但願對大家有所幫助。面試

老規矩,先上一道題給你們看看單調棧能解決什麼樣的問題,這題是 2020 年猿輔導(K12 教育的獨角獸,研發崗白菜價 40W 起步,不加班高福利,想要內推的能夠私信老汪)的一道面試題。算法

給定一個二叉搜索樹,並給出他的前序序列,要求輸出中序序列,時間複雜度O(n),並給出證實。

單調棧

  • 是什麼:單調棧是一種具備單調性的棧,任什麼時候刻從棧頂到棧底的元素都是單調增/減的。
  • 幹什麼:數組

    • 單調棧能夠找到從左/右遍歷第一個比它大/小的元素的位置。
    • 單調棧也能夠將某個元素左/右邊全部比它小/大的元素按升/降序輸出。
  • 怎麼作:使用棧結構,在入棧的時候保持 id 和 val 的單調性便可。

翻譯成大白話,凡是遇到題目中直接或間接要求查找某個元素左/右邊第一個大於/小於它的元素,或者要求將某個元素左/右邊全部小/大於它的元素按升/降序輸出,就可使用單調棧來解決。數據結構

具體怎麼作你可能還有些迷糊,下面咱們直接經過作題來加深對單調棧的理解。十分鐘包教包會,當天就能夠和麪試官對線。函數

初入茅廬

給定數組 a,要求輸出這樣的數組 b,b[i] 是 a[i] 左邊第一個比 b[i] 大的元素,若不存在則 b[i] = a[i]。

最暴力的解法,就是對每個 a[i],遍歷其左邊的全部元素,這樣所需的時間複雜度是 O(n^2)。若是使用單調棧,時間複雜度能夠優化到 O(n)。優化

這是最基本、最直白的單調棧問題,全部單調棧問題都是在該問題上進行延伸、變種得來的,掌握了這個問題的解決方法,全部單調棧的問題都能迎刃而解this

因爲本問題過於簡單、直白,就很少作講解,直接上代碼:spa

public int[] solve(int[] a){
  if(a == null) return null; //容錯處理,越是簡單的題越要注意細節
  int n = a.length;
  int[] b = new int[n];
  Stack<Integer> stack = new Stack(); //單調棧固然要用棧結構啦
  for(int i = 0; i < n; i++){
    while(!stack.isEmpty() && stack.peek() < a[i]) stack.pop(); //全部比 a[i] 小的元素都出棧,保證了從棧頂到棧底元素是單調增的,而且棧頂的元素是 a[i] 左邊第一個比 a[i] 大的元素
    if(stack.isEmpty()) stack.push(a[i]);
    b[i] = stack.peek();
  }
  return b;
}

這代碼也是單調棧問題的基本結構,全部單調棧問題的代碼都基於此進行變種、擴展。小夥伴們能夠多花兩分鐘好好消化上面的代碼。翻譯

考慮到有些小夥伴不用 Java,老汪把上述代碼抽象成僞代碼,這也是單調棧的基本結構

背誦 + 套用,便可解決全部單調棧問題。

函數 solve (數組 a):
    新建數組 b;
    新建棧 stack;
    For i From 0 To n - 1:
        While 棧非空 且 棧頂元素 < a[i]:
            出棧;
        End While
        If 棧空 Then:
            a[i] 入棧;
        End If
        b[i] = 棧頂元素;
    End For
    return b;

小試牛刀

單調棧的最基本使用方式咱們已經瞭解了,下面一塊兒來解決文章開頭提到的面試題。

給定一個二叉搜索樹,並給出他的前序序列,要求輸出中序序列,時間複雜度O(n),並給出證實。

最暴力的方法就是對前序序列進行排序,時間複雜度爲 O(nlogn),使用單調棧能夠優化到 O(n)。

思路分析:

對於二叉搜索樹而言,給定其根節點 a[i],左子樹裏全部元素都比 a[i] 小,右子樹裏全部元素都比 a[i] 大。

回顧一下遍歷順序:

前序序列,先遍歷根節點,再遍歷左子樹,最後遍歷右子樹;

中序序列,先遍歷左子樹,再遍歷根節點,最後遍歷右子樹。

即,對於元素 a[i],以它爲根節點的子樹,

前序序列爲,a[i], a[i] 的左子樹序列, a[i] 的右子樹序列

後序序列爲,a[i] 的左子樹序列, a[i]a[i] 的右子樹序列

所以,當咱們遍歷前序序列,遇到右子樹的第一個元素 a[j] 時,其左邊全部元素都小於 a[j], 將其左邊全部元素按升序輸出,便可獲得 a[i]a[i] 的左子樹 的後序序列。

對於右子樹再以上述步驟迭代,便可獲得完整的後序序列。

顯然,這是單調棧的第二種用法:單調棧能夠將某個元素左邊全部比它小的元素按升序輸出。

下面直接上代碼:

public int[] solve(int[] pre){
  if(pre == null) return null; //注意容錯細節
  int n = pre.length;
  int[] infix = new int[n]; //中序序列
  Stack<Integer> stack = new Stack();
  int index = 0; // 指示中序序列的當前下標
  for(int i = 0; i < n; i++){
    while(!stack.isEmpty() && stack.peek() < pre[i]) infix[index++] = stack.pop(); //因爲棧中元素是從棧頂到棧底單調增的,因此能夠保證輸出序列是單調增的
    stack.push(pre[i]);
  }
  while(!stack.isEmpty()) infix[index++] = stack.pop();
  return infix;
}

打怪升級

再來看一道題,這道題是 2020 年 9 月 6 日字節筆試的第二題。難度對標 leetcode 的 medium 級別。

給定一個長爲 n 的序列 a。L[i] 表示第 i 個位置左邊第一個大於 a[i] 的數的下標(從 1 開始),沒有的話爲 L[i] = 0。R[i] 表示第 i 個位置右邊第一個大於 a[i] 的數的下標(從 1 開始),沒有的話爲 R[i] = 0。求 $max_{i = 1}^{n}L[i]·R[i]$。

思路分析:一看題目,就是單調棧的第一種用法。使用兩次單調棧分別獲得 L[i] 和 R[i],再遍歷一遍便可。時間複雜度爲 O(n)。

代碼以下:

public int solve(int[] a){
  if(a == null) throw new RuntimeException("輸入有誤!"); //細節,必定要細
  int n = a.length;
  int[] L = new int[n], R = new int[n];
  // 這裏分兩次 for 循環來寫,熟練的話能夠放在同一個 for 循環裏。
  Stack<Pair> stack = new Stack();
  for(int i = 0; i < n; i++){
    while(!stack.isEmpty() && stack.peek().val < a[i]) stack.pop();
    L[i] = stack.isEmpty() ? 0 : stack.peek().id + 1; //注意題目要求下標是從 1 開始的
    stack.push(new Pair(i, a[i]));
  }
  stack.clear();
  for(int i = n - 1; i >= 0; i--){
    while(!stack.isEmpty() && stack.peek().val < a[i]) stack.pop();
    R[i] = stack.isEmpty() ? 0 : stack.peek().id + 1; //注意題目要求下標是從 1 開始的
    stack.push(new Pair(i, a[i]));
  }
  // L[i] 和 R[i] 計算完畢,下面遍歷一遍獲得最大值便可
  int ans = 0;
  for(int i = 0; i < n; i++) ans = Math.max(ans, L[i] * R[i]);
  return ans;
}

class Pair{
  int id; // 下標
  int val;
  public Pair(int id, int val){
    this.id = id;
    this.val = val;
  }
}

出師試煉

關卡 1

給定一個整數數組,你須要驗證它是不是一個二叉搜索樹正確的先序遍歷序列。

你能夠假定該序列中的數都是不相同的。

關卡 2

給定一個以字符串表示的非負整數 num,移除這個數中的 k 位數字,使得剩下的數字最小。

關卡 3

給定 n 個非負整數,用來表示柱狀圖中各個柱子的高度。每一個柱子彼此相鄰,且寬度爲 1 。

求在該柱狀圖中,可以勾勒出來的矩形的最大面積。

PS:在公衆號【往西汪】後臺回覆關鍵字【單調棧】,便可得到上述關卡的過關祕籍(代碼實現)。

本期單調棧問題就分享到這,下一期你想看什麼內容呢?單調隊列?前綴和思想?仍是老汪獨家刷題祕籍?又或者有其餘想看的,也能夠在下方評論區告訴老汪。

我是往西汪,致力於面向 offer 分享算法知識,但願你早日收穫心儀 offer。咱們下期再見。

愛大家

相關文章
相關標籤/搜索