从栈到单调栈

从栈到单调栈

0、什么是栈?

用一个很贴切的例子,简述一下什么是栈:

想象一摞叠在一起的盘子。

我们平时放盘子的时候,都是从下往上一个一个放;取的时候,我们也是从上往下一个一个地依次取,不能从中间任意抽出。后进者先出,先进者后出,这就是典型的“栈”结构

在这里插入图片描述

从栈的操作特性上来看,栈是一种“操作受限”的线性表,只允许在一端插入和删除数据。

至于其如何实现,以及更多元的应用方面,这里就不再阐述。(好多大佬的资源,小弟看后真感自己才疏学浅.jpg)

1、什么是单调栈?

那有了栈的概念后,什么是单调栈。

所谓单调栈,也就是存放有序数据的栈,为此单调栈也分为单调递增栈单调递减栈

  • 单调递增栈:单调递增栈就是从栈底到栈顶数据是从大到小
  • 单调递减栈:单调递减栈就是从栈底到栈顶数据是从小到大

名字听上去可能也不太好记,这里我为大家画了个图来理解记忆一下:

在这里插入图片描述

2、单调栈应用和伪代码

理解单调栈的概念后,我们如何在应用中使用这种数据结构?又或者在什么情况下适合使用这种数据结构呢?

先回答后一个问题。

在这一段时间的LeetCode之旅中,我发现这样一大类问题适合用单调栈解决:

比当前元素更大(小)的前(后)一个元素。

举个例子说明一下。

这里有一个数组,其假定为[5, 6, 8, 2, 7, 4],我们要在其中寻找从左向右第一个小于5的元素。

那么,你可能会想,遍历一次不久好了,算法复杂度也才O(n)。

可是如果,改成对数组中的所有元素都要做类似处理呢?那上面的算法不就是O(n2)了吗。

所以借用这个例子来阐述一下单调栈的思想。单调栈解决这道题目的算法复杂度时O(n)的。(你也可以自己先思考一下,结合前面讲的单调栈的原理)这里我给大家画了一幅图,你可以尝试着看一下。
在这里插入图片描述

可以看到一个典型现象是,我们一直都保持栈中的数据从栈底到栈顶是递增的。遇到比栈顶元素大的,我们就将其压入;比栈顶元素小的,我们就将栈顶元素弹出,并且将该元素(非栈顶元素的比较元素)记录为栈顶元素的第一个小于其的元素。

并且值得注意的是,这个比较的过程是持续进行的,也就是当栈顶元素不严格小于当前元素时,才停止下来。

为了让你更加明白上述处理的过程,这里我针对该问题谢了一段代码,你可以配合图看下是否更好理解。

int nums[6] = {5, 6, 8, 2, 7, 4};

int ans[6] = {-1}; // 用于存储结果的向量

stack<int> data; // 用来模拟单调栈

for (int i=0; i<6; i++) {
    while (!data.empty() && nums[data.top] > nums[i]){
        int index = data.top(); // 取出栈顶元素
        data.pop(); // 栈顶元素出栈
        ans[index] = nums[i]; // 进行操作
    }
    data.push(i);// 当前元素入栈
}

或许你在有了上面的思考后,对单调栈应该有了初步的印象和理解。那么就单调栈的应用,或许可以给出其大致的运行框架,日后刷题后遇到也就处理部分不同,稍微变动一下也八九不离十。给出模板如下:

stack<int> st; // 用于模拟单调栈
for (循环遍历数组){
    while (栈中有元素 && 当前元素与栈顶元素满足一定关系(需判断是哪种单调栈)){
        // 取出栈顶元素,并且栈顶元素出栈
        // 进行操作
    }
    // 当前元素入栈
}

对于该框架,你可以对照前面一个例子看看,再熟悉一下,后面会有几个例题,进一步加深理解和记忆。

3、单调栈的应用例题

3.1、视野总和

3.1.1、题目描述

描叙:有n个人站队,所有的人全部向右看,个子高的可以看到个子低的发型,给出每个人的身高,问所有人能看到其他人发现总和是多少。
输入:4 3 7 1
输出:2
解释:个子为4的可以看到个子为3的发型,个子为7可以看到个子为1的身高,所以1+1=2

3.1.2、解题思路

在这里插入图片描述

  • 需要设置一个单调栈(从栈底到栈顶数据大小递减)
  • 如果栈为空或者当前元素大于栈顶元素,则将该元素入栈
  • 反之,循环将栈顶元素弹出,并计算栈顶所能看到比其小的元素的个数,累加进结果,直到栈顶元素不再严格小于当前指向元素

3.1.3、结果代码

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++)
	{
        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、柱状图中的最大矩形

3.2.1、题目描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.2.2、解题思路

3.2.2.1、双重循环
  • 对于每一个矩形,可以分别向左或者向右扩展其最大宽度。
  • 用这个最大宽度与高度的乘积作比较,保存大的作为结果。
3.2.2.2、单调栈

这道题可以用单调栈来解题,并且是单调栈比较典型的应用。相信有了前面的基础,这道题的大致框架是不是有在脑海中呢,再来一起回顾一下。

  • 我们需要借助栈,来完成单调栈的模拟(初始化一个数据结构栈)
  • 依次查看每个元素(一个for循环遍历)
  • 在查看的过程中,如果栈不是空的,并且当前元素与栈顶元素满足一定关系,我们将做处理
  • 否则,我们将该元素压入栈中

那么对于该题,如何利用这个框架解题呢,这得具体看这个问题,可以抽象出哪种模型。还记得前面所讲的,一个大类,比当前元素更大(小)的前(后)一个元素吗,仔细思考一下。

可以发现,某一个高度可以组成的最大矩形,其左右两边必然是连续的并且大于等于其高度的矩形的集合。

有点绕口,我给你画个图,你尝试这理解一下。

这里有图啊

那么有了这个基础后,在单调栈中,栈顶元素的左边第一个元素,一定是比其小的(大于等于其的已经弹出,或者左边第一个元素本身就比其小),而右边也一定是比起小的(遇到了比其小的元素,此时需要对其做处理了。当然,这也表明在前面比其大的元素已经全部弹出,或者当前元素本身就比栈顶元素小)。那么这样,隐式的,可以通过下标来计算这个最大矩形的宽度,最后求出面积。这里你可以结合我画的图,再看一下。

所以,上面的理论,再结合单调栈的框架,我们来看看其具体处理过程:

  • 我们需要借助栈,来完成单调栈的模拟(初始化一个数据结构栈)
  • 依次查看每个元素(一个for循环遍历)
  • 在查看的过程中,如果栈不是空的,并且当前元素小于栈顶元素所代表的值,我们将弹出并保存栈顶元素所代表的矩当前最大矩形高度,然后将其与宽度相乘,并与保存的最大值作比较,并更新。
  • 否则,我们将该元素(元素下标)压入栈中

当然这里,还需要哨兵的存在,才能处理好边界调节。这里可以看下这副图,也许更好理解。

这里有图。

3.2.2.3、结果代码

结合上面的思想,再来看看代码,也许更清晰。

class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        heights.push_back(0);
        heights.insert(heights.begin(), 0);

        int n = heights.size();

        stack<int> data; // 借助栈来模拟单调栈

        int ans = 0;

        for (int i=0; i<n; i++) {
            while (!data.empty() && heights[data.top()] > heights[i]) {
                int index = data.top();
                data.pop();

                ans = max(ans, heights[index] * (i - data.top() - 1));
            }
            data.push(i);
        }
        
        return ans;
    }
};

4、总结

单调栈这里就结束了,核心思想也就是单调和栈两部分,时刻谨记在栈中数据为有序的,这个数据结构也就明白一半了。剩下的一半,需要各位在实践中找真理了。

这里将LeetCode上可以利用单调栈解决的题目链接给出,大家可以多多练习,实践出真理。

LeetCode 496. 下一个更大元素 I

LeetCode 121. 买卖股票的最佳时机

LeetCode 503. 下一个更大元素 II

LeetCode 739. 每日温度

LeetCode 84. 柱状图中最大的矩形

LeetCode 42. 接雨水

这些不是全部,今后会继续补充,题解也才写了两道,不够完善。

初出茅庐,也请各位大佬不吝赐教,有误的地方帮忙指出,万分感谢。

5、致谢引用

LeetCode 单调栈问题总结

数据结构–单调栈

《数据结构与算法之美》-- 王争

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Python中,单调栈和单调队列是两种不同的数据结构单调栈是一个栈,它的特点是栈内的元素是单调的,可以是递增或递减的。在构建单调栈时,元素的插入和弹出都是在栈的一端进行的。与此类似,单调队列也是一个队列,它的特点是队列内的元素是单调的,可以是递增或递减的。在构建单调队列时,元素的插入是在队列的一端进行的,而弹出则是选择队列头进行的。 单调队列在解决某些问题时,能够提升效率。例如,滑动窗口最大值问题可以通过使用单调队列来解决。单调队列的结构可以通过以下代码来实现: ```python class MQueue: def __init__(self): self.queue = [] def push(self, value): while self.queue and self.queue[-1 < value: self.queue.pop(-1) self.queue.append(value) def pop(self): if self.queue: return self.queue.pop(0) ``` 上述代码定义了一个名为MQueue的类,它包含一个列表作为队列的存储结构。该类有两个方法,push和pop。push方法用于向队列中插入元素,它会删除队列尾部小于插入元素的所有元素,并将插入元素添加到队列尾部。pop方法用于弹出队列的头部元素。 总结来说,单调栈和单调队列都是为了解决特定问题而设计的数据结构单调栈在构建时元素的插入和弹出都是在栈的一端进行的,而单调队列则是在队列的一端进行的。在Python中,可以通过自定义类来实现单调队列的功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值