單調棧二三事
ps:整合所得,第一次釋出在個人網站(http://zhangyihao.top/2021/05/05/dan-diao-zhan-er-san-shi/)上,歡迎來玩orz
1.什麼是單調棧?
從名字上就聽的出來,單調棧中存放的資料應該是有序的,所以單調棧也分為單調遞增棧和單調遞減棧
- 單調遞增棧:單調遞增棧就是從棧底到棧頂資料是從大到小
- 單調遞減棧:單調遞減棧就是從棧底到棧頂資料是從小到大
2.模擬單調棧的資料push和pop
模擬實現一個單調遞增棧:
現在有一組數10,3,7,4,12。從左到右依次入棧,則如果棧為空或入棧元素值小於棧頂元素值,則入棧;否則,如果入棧則會破壞棧的單調性,則需要把比入棧元素小的元素全部出棧。單調遞減的棧反之。
10入棧時,棧為空,直接入棧,棧內元素為10。
3入棧時,棧頂元素10比3大,則入棧,棧內元素為10,3。
7入棧時,棧頂元素3比7小,則棧頂元素出棧,此時棧頂元素為10,比7大,則7入棧,棧內元素為10,7。
4入棧時,棧頂元素7比4大,則入棧,棧內元素為10,7,4。
12入棧時,棧頂元素4比12小,4出棧,此時棧頂元素為7,仍比12小,棧頂元素7繼續出棧,此時棧頂元素為10,仍比12小,10出棧,此時棧為空,12入棧,棧內元素為12。
虛擬碼:
stack<int> st;
//此處一般需要給陣列最後新增結束標誌符,具體下面例題會有詳細講解
for (遍歷這個陣列)
{
if (棧空 || 棧頂元素大於等於當前比較元素)
{
入棧;
}
else
{
while (棧不為空 && 棧頂元素小於當前元素)
{
棧頂元素出棧;
更新結果;
}
當前資料入棧;
}
}
3.單調棧的應用
單調棧主要解決下面幾種問題
- 比當前元素更大的下一個元素
- 比當前元素更大的前一個元素
- 比當前元素更小的下一個元素
- 比當前元素更小的前一個元素
單調棧的應用我們直接拿一些具體的題來對照應用:
3.1.視野總和
描敘:有n個人站隊,所有的人全部向右看,個子高的可以看到個子低的髮型,給出每個人的身高,問所有人能看到其他人發現總和是多少。
輸入:4 3 7 1
輸出:2
解釋:個子為4的可以看到個子為3的髮型,個子為7可以看到個子為1的身高,所以1+1=2
1.設定一個單調遞增的棧(棧內0~n為單調遞減)
2.當遇到大於棧頂的元素,開始更新之前不高於當前人所能看到的值
程式碼:
int FieldSum(vector<int>& v)
{
v.push_back(INT_MAX);/這裡可以理解為需要一個無限高的人擋住棧中的人,不然棧中元素最後無法完全出棧
stack<int> st;
int sum = 0;
for (int i = 0; i < (int)v.size(); i++)
{
if (st.empty() || v[st.top()] > v[i])//小於棧頂元素入棧
{
st.push(i);
}
else
{
while (!st.empty() && v[st.top()] <= v[i])
{
int top = st.top();//取出棧頂元素
st.pop();
sum += (i - top - 1);//這裡需要多減一個1
}
st.push(i);
}
}
return sum;
}
3.2.接雨水
給定 n 個非負整數表示每個寬度為 1 的柱子的高度圖,計算按此排列的柱子,下雨之後能接多少雨水。
輸入: [0,1,0,2,1,0,1,3,2,1,2,1]
輸出: 6
維護一個單調棧,單調棧儲存的是下標,滿足從棧底到棧頂的下標對應的陣列 $\textit{height} $中的元素遞減。
從左到右遍歷陣列,遍歷到下標 i 時,如果棧內至少有兩個元素,記棧頂元素為top,\textit{top}top 的下面一個元素是left,則一定有 height [ left ] ≥ height [ top ] \textit{height}[\textit{left}] \ge \textit{height}[\textit{top}] height[left]≥height[top]。如果 height [ i ] > height [ top ] \textit{height}[i]>\textit{height}[\textit{top}] height[i]>height[top],則得到一個可以接雨水的區域,該區域的寬度是$ i-\textit{left}-1$,高度是 min ( height [ left ] , height [ i ] ) − height [ top ] ) \min(\textit{height}[\textit{left}],\textit{height}[i])-\textit{height}[\textit{top}]) min(height[left],height[i])−height[top])],根據寬度和高度即可計算得到該區域能接的雨水量。
為了得到 left \textit{left} left,需要將 top \textit{top} top 出棧。在對 top \textit{top} top計算能接的雨水量之後, left \textit{left} left 變成新的 top \textit{top} top,重複上述操作,直到棧變為空,或者棧頂下標對應的$ \textit{height}$中的元素大於或等於 height [ i ] \textit{height}[i] height[i]。在對下標 i處計算能接的雨水量之後,將 i入棧,繼續遍歷後面的下標,計算能接的雨水量。遍歷結束之後即可得到能接的雨水總量。
class Solution {
public:
int trap(vector<int>& height) {
int ans = 0;
stack<int> stk;
int n = height.size();
for (int i = 0; i < n; ++i) {
while (!stk.empty() && height[i] > height[stk.top()]) {
int top = stk.top();
stk.pop();
if (stk.empty()) {
break;
}
int left = stk.top();
int currWidth = i - left - 1;
int currHeight = min(height[left], height[i]) - height[top];
ans += currWidth * currHeight;
}
stk.push(i);
}
return ans;
}
};
連結:https://leetcode-cn.com/problems/trapping-rain-water/solution/jie-yu-shui-by-leetcode-solution-tuvc/
來源:力扣(LeetCode)
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。
3.3.每日溫度
根據每日 氣溫 列表,請重新生成一個列表,對應位置的輸入是你需要再等待多久溫度才會升高超過該日的天數。如果之後都不會升高,請在該位置用 0 來代替。
例如,給定一個列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的輸出應該是 [1, 1, 4, 2, 1, 1, 0, 0]。
根據題意,將列表反轉之後,我們所求的就是從當前元素e開始左側第一個比e的元素,因此可以使用遞增棧處理
var dailyTemperatures = function (T) {
T.reverse()
var stack = []
var ans = []
for(var i = 0; i < T.length; ++i){
// 維護遞增棧,注意此處棧內儲存的是索引值而不是具體的溫度
while(stack.length && T[stack[stack.length - 1]] <= T[i]){
stack.pop()
}
if(stack.length){
// 左側棧中的元素都比當前元素值大,最近一天則取棧頂元素即可
ans[i] = i - stack[stack.length - 1]
}else {
ans[i] = 0
}
stack.push(i)
}
return ans.reverse()
};
上面的程式碼進行了兩次翻轉,我們可以將其簡化
var dailyTemperatures = function (T) {
var ans = new Array(T.length).fill(0)
var stack = []
for(var i = 0; i < T.length; ++i){
// 當碰到一個大於棧頂的元素時,則該元素一定是離棧頂元素最近一天的較高值
while(stack.length && T[stack[stack.length - 1]] < T[i]){
var cur = stack.pop()
ans[cur] = i - cur
}
stack.push(i)
}
return ans
}
3.4. 下一個更大的元素
思路:從後向前構建一個遞增棧,並儲存每一個元素的右側最大值
var nextGreaterElement = function(nums1, nums2) {
var stack = []
var map = {}
for(var i = nums2.length - 1; i >= 0; --i){
var cur = nums2[i]
while(stack.length && top(stack) < cur){
stack.pop()
}
map[cur] = stack.length ? top(stack) : -1
stack.push(cur)
}
return nums1.map(item=>map[item])
function top(arr){
return arr[arr.length - 1]
}
};
這個題的思路與上面類似,只是增加了迴圈陣列的判斷,因此可以先判斷正向,然後再拼接判斷一次
3.5. 最長寬度坡
給定一個整數陣列 A,坡是元組 (i, j),其中 i < j 且 A[i] <= A[j]。這樣的坡的寬度為 j - i。
找出 A 中的坡的最大寬度,如果不存在,返回 0 。
本題主要的思路是:正向構造一個遞增棧,棧中儲存的都是可以作為“坡底”元素的索引值,然後反向從末尾與棧頂元素對應的值進行比較,同時彈出棧頂元素,取較大值作為返回結果
var maxWidthRamp = function (A) {
var stack = []
// 構建遞增棧,棧中元素
for(var i = 0; i < A.length; ++i){
if(!stack.length || A[stack[stack.length - 1]] > A[i]){
stack.push(i)
}
}
var ans = 0
for(var i = A.length - 1; i >=0; --i){
while(stack.length && A[stack[stack.length - 1]] <= A[i]){
ans = Math.max(i - stack.pop(), ans)
}
}
return ans
};
3.6. 表現良好的最長時間段
給你一份工作時間表 hours,上面記錄著某一位員工每天的工作小時數。
我們認為當員工一天中的工作小時數大於 8 小時的時候,那麼這一天就是「勞累的一天」。
所謂「表現良好的時間段」,意味在這段時間內,「勞累的天數」是嚴格 大於「不勞累的天數」。
請你返回「表現良好時間段」的最大長度。
輸入:hours = [9,9,6,0,6,6,9]輸出:3解釋:最長的表現良好時間段是 [9,9,6]。
本題主要思路為
- 將hours轉換成score陣列,表現良好為1,不良為-1,則題目轉換為:score中和大於0的連續子陣列的最大長度
- 使用字首和,字首和陣列中,
presum[j] - presum[i]
表示i,j
區間的元素和,因此題目轉換為:presum 中兩個索引 i 和 j,使j - i
最大,且保證presum[j] - presum[i] > 0
- 此時與上面的最長坡問題類似,通過單調棧求解最大長度
var longestWPI = function(hours) { // 題目轉換為:求score中和大於0的連續子陣列的最大長度 var score = hours.map(item=>{ return item > 8 ? 1 : -1 }) // 使用字首和 var presum = [0] score.forEach((item, index)=>{ presum.push(presum[index] + item) }) // 題目轉換為求:presum 中兩個索引 i 和 j,使 j - i 最大,且保證 presum[j] - presum[i] > 0, var stack = [] for(var i = 0; i < presum.length; ++i){ if(!stack.length || presum[stack[stack.length - 1]] > presum[i]){ stack.push(i) } } var ans = 0 for(var i = presum.length-1; i >=0; --i){ while(stack.length && presum[stack[stack.length - 1]] < presum[i]){ ans = Math.max(ans, i - stack.pop()) } } return ans};
3.7. 股票價格跨度
思路為:儲存一個遞增棧,當插入新元素時,與棧頂元素做比較並依次彈出,找到第一個大於當前元素的索引值,返回結果
var StockSpanner = function() {
this.data = []
this.stack = []
};
// 使用單調棧
StockSpanner.prototype.next = function(price) {
var arr = this.data
var stack = this.stack
arr.push(price)
var i = arr.length
// 儲存一個遞增棧,當插入新元素時,與棧頂元素做比較並依次彈出,找到第一個大於當前元素的索引值,返回結果
while(stack.length && arr[stack[stack.length - 1]] <= price){
stack.pop()
}
var ans = i - (stack.length ? stack[stack.length - 1] + 1 : 0)
stack.push(i-1)
return ans
};
// 輸入 [100, 80, 60, 70, 60, 75, 85]
// 輸出 [1, 1, 1, 2, 1, 4, 6]
// [100] [100, 80] [100, 80, 60] [100, 80, 70] [100,80,60] [100,80,75] [100,85]
小結
本文主要總結了單調棧的概念和一些應用場景,大致瞭解了利用單調棧來解決求前後較大值、較小值的一些方法。
參考連結:
1.https://www.shymean.com/article/%E5%8D%95%E8%B0%83%E6%A0%88%E5%8F%8A%E5%85%B6%E5%BA%94%E7%94%A8
2.[https://blog.csdn.net/lucky52529/article/details/89155694]
3.力扣官方題解