1. 程式人生 > 其它 >單調棧二三事

單調棧二三事

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

思路:觀察題之後,我們發現實際上題目轉化為找當前數字向右查詢的第一個大於他的數字之間有多少個數字,然後將每個          結果累計就是答案,但是這裡時間複雜度為O(N^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.接雨水

leetcode傳送門

給定 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.每日溫度

letcode傳送門

根據每日 氣溫 列表,請重新生成一個列表,對應位置的輸入是你需要再等待多久溫度才會升高超過該日的天數。如果之後都不會升高,請在該位置用 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. 下一個更大的元素

letcode傳送門:496. 下一個更大元素 I

思路:從後向前構建一個遞增棧,並儲存每一個元素的右側最大值

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]
    }
};

letcode傳送門:503. 下一個更大元素 II

這個題的思路與上面類似,只是增加了迴圈陣列的判斷,因此可以先判斷正向,然後再拼接判斷一次

letcode傳送門:556. 下一個更大元素 III

3.5. 最長寬度坡

letcode傳送門:962. 最大寬度坡

給定一個整數陣列 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. 表現良好的最長時間段

letcode傳送門:1124. 表現良好的最長時間段

給你一份工作時間表 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. 股票價格跨度

letcode傳送門:901. 股票價格跨度

思路為:儲存一個遞增棧,當插入新元素時,與棧頂元素做比較並依次彈出,找到第一個大於當前元素的索引值,返回結果

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.力扣官方題解