单调栈二三事

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值