1. 程式人生 > 實用技巧 >「面向 offer 學演算法」筆面試大殺器 -- 單調棧

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

目錄

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

前言

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

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

老規矩,先上一道題給大家看看單調棧能解決什麼樣的問題,這題是 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)。

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

由於本問題過於簡單、直白,就不多做講解,直接上程式碼:

publicint[]solve(int[]a){
if(a==null)returnnull;//容錯處理,越是簡單的題越要注意細節
intn=a.length;
int[]b=newint[n];
Stack<Integer>stack=newStack();//單調棧當然要用棧結構啦

for(inti=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();
}
returnb;
}

這程式碼也是單調棧問題的基本結構,所有單調棧問題的程式碼都基於此進行變種、擴充套件。小夥伴們可以多花兩分鐘好好消化上面的程式碼。

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

背誦 + 套用,即可解決所有單調棧問題。

函式solve(陣列a):
新建陣列b;
新建棧stack;
ForiFrom0Ton-1:
While棧非空且棧頂元素<a[i]:
出棧;
EndWhile
If棧空Then:
a[i]入棧;
EndIf
b[i]=棧頂元素;
EndFor
returnb;

小試牛刀

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

給定一個二叉搜尋樹,並給出他的前序序列,要求輸出中序序列,時間複雜度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] 的左子樹 的後序序列。

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

顯然,這是單調棧的第二種用法:單調棧可以將某個元素左邊所有比它小的元素按升序輸出。

下面直接上程式碼:

publicint[]solve(int[]pre){
if(pre==null)returnnull;//注意容錯細節
intn=pre.length;
int[]infix=newint[n];//中序序列
Stack<Integer>stack=newStack();
intindex=0;//指示中序序列的當前下標
for(inti=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();
returninfix;
}

打怪升級

再來看一道題,這道題是 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。求

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

程式碼如下:

publicintsolve(int[]a){
if(a==null)thrownewRuntimeException("輸入有誤!");//細節,一定要細
intn=a.length;
int[]L=newint[n],R=newint[n];
//這裡分兩次for迴圈來寫,熟練的話可以放在同一個for迴圈裡。
Stack<Pair>stack=newStack();
for(inti=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(newPair(i,a[i]));
}
stack.clear();
for(inti=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(newPair(i,a[i]));
}
//L[i]和R[i]計算完畢,下面遍歷一遍得到最大值即可
intans=0;
for(inti=0;i<n;i++)ans=Math.max(ans,L[i]*R[i]);
returnans;
}

classPair{
intid;//下標
intval;
publicPair(intid,intval){
this.id=id;
this.val=val;
}
}

出師試煉

關卡 1

給定一個整數陣列,你需要驗證它是否是一個二叉搜尋樹正確的先序遍歷序列。

你可以假定該序列中的數都是不相同的。

關卡 2

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

關卡 3

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

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

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

本期單調棧問題就分享到這,下一期你想看什麼內容呢?單調佇列?字首和思想?還是老汪獨家刷題祕籍?又或者有其他想看的,也可以在下方評論區告訴老汪。

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