单调栈---高低高问题的神器

本文介绍了单调栈的基本概念,包括单调递增栈和单调递减栈的工作原理,通过模拟和实际问题(如视野总和、132模式、柱状图最大矩形)展示了其在算法中的应用。还涉及到了单调队列的概念,对比了单调栈与队列在特定问题上的解决方案。
摘要由CSDN通过智能技术生成


前言

哟哟哟,这不单调栈吗?几天不见这么拉了。
哈?你说单调栈是啥?
其实之前我也不知道,直到前几天在力扣做了道题才接触到单调栈这一数据结构,开始还有点看不懂;后面搞清楚之后发现这玩意在一些类型的题目上特别好用有木有。

一、单调栈是什么?

顾名思义,单调栈中存放的数据应该是有序的,所以单调栈也分为单调递增栈和单调递减栈

(1)单调递增栈:单调递增栈就是从栈底到栈顶数据是从大到小
(2)单调递减栈:单调递减栈就是从栈底到栈顶数据是从小到大

是不是感觉有点怪,其实我也感觉其递增递减的方向应该是反过来的,但没办法,人家就是这么定义滴。

二、模拟单调栈的使用

下面模拟一个单调递增栈

现在有一组数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 (栈不为空 && 栈顶元素小于当前元素)
		{
			栈顶元素出栈;
			更新结果;
		}
		当前数据入栈;
	}
}

三、单调栈的应用

单调栈一般用于处理“高低高”问题,而且很多时候为了让栈内数据强制出栈,会在给定的数据前面或后面加入一个极大值或者极小值

下面我们通过几道题目来看一下单调栈的应用。

1.视野总和
难度:简单
描叙:有n个人站队,所有的人全部向右看,个子高的可以看到个子低的发型,给出每个人的身高,问所有人能看到其他人发现总和是多少。
输入:4 3 7 1
输出:2
解释:个子为4的可以看到个子为3的发型,个子为7可以看到个子为1的身高,所以1+1=2
思路:观察题之后,我们发现实际上题目转化为找当前数字向右查找的第一个大于他的数字之间有多少个数字,然后将每个 结果累加就是答案,我们很容易想到两次循环的暴力法,但时间复杂度为O(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;
}

2.132模式
难度:中等
描述:给你一个整数数组 nums ,数组中共有 n 个整数。132 模式的子序列 由三个整数 nums[i]、nums[j] 和 nums[k] 组成,并同时满足:i < j < k 和 nums[i] < nums[k] < nums[j] 。
如果 nums 中存在 132 模式的子序列 ,返回 true ;否则,返回 false 。

示例:
输入:nums = [3,1,4,2]
输出:true
解释:序列中有 1 个 132 模式的子序列: [1, 4, 2] 。

最朴素的方法就是暴力三次循环,但时间复杂度是O(n^3),用脚都知道肯定超时了,即使优化成枚举其中两个数,O(n ^2)的时间复杂度在数据量一大的时候还是很容易超时。这里我们看到要求的是132这样的高低高数据,看到高低高我们就容易想到是不是可以用单调栈来做。

思路:
我们可以从 132 的大小特性去分析,如果在确定一个数之后,如何快速找到另外两个数(我们使用 ijk 来代指 132 结构):

枚举 i:由于 i 是 132 结构中最小的数,那么相当于我们要从 i 后面,找到一个对数 (j,k),使得 (j,k) 都满足比 i 大,同时 j 和 k 之间存在 j > k 的关系。由于我们的遍历是单向的,因此我们可以将问题转化为找 k,首先 k 需要比 i 大,同时在 [i, k] 之间存在比 k 大的数即可。

枚举 j:由于 j 是 132 结构里最大的数,因此我们需要在 j 的右边中比 j 小的「最大」的数,在 j 的左边找比 j 小的「最小」的数。这很容易联想到单调栈,但是朴素的单调栈是帮助我们找到左边或者右边「最近」的数,无法直接满足我们「最大」和「最小」的要求,需要引入额外逻辑。

枚举 k:由于 k 是 132 结构中的中间值,这里的分析逻辑和「枚举 i」类似,因为遍历是单向的,我们需要找到 k 左边的 i,同时确保 [i,k] 之间存在比 i 和 k 大的数字。

以上三种分析方法都是可行的,但「枚举 i」的做法是最简单的。

因为如果存在 (j,k) 满足要求的话,我们只需要找到一个最大的满足条件的 k,通过与 i 的比较即可。

先说处理过程吧,我们从后往前做,维护一个「单调递减」的栈,同时使用 k 记录所有出栈元素的最大值(k 代表满足 132 结构中的 2)。

当我们遇到需要弹栈的时候,说明当前数据不再符合单调递减,即出现了比栈顶元素大的j;也就是出现了[j,k]对,这时我们只需要不断出栈继续维护单调递减,在出栈的元素中找出最大值。为什么要找最大值,因为k是132中的次大值,以及出现了[j,k]对,我们自然是找越大的2,保证不会错过满足条件的1.

那么当我们遍历到 i,只要满足发现满足 nums[i] < k,说明我们找到了符合条件的 i j k。

举个🌰,对于样例数据 [3, 1, 4, 2],我们知道满足 132 结构的子序列是 [1, 4, 2],其处理逻辑是(遍历从后往前):

枚举到 2:栈内元素为 [2],k = INF
枚举到 4:不满足「单调递减」,2 出栈更新 k,4 入栈。栈内元素为 [4],k = 2
枚举到 1:满足 nums[i] < k,说明对于 i 而言,后面有一个比其大的元素(满足 i < k 的条件),同时这个 k 的来源又是因为维护「单调递减」而弹出导致被更新的(满足 i 和 k 之间,有比 k 要大的元素)。因此我们找到了满足 132 结构的组合。
这样做的本质是:我们通过维护「单调递减」来确保已经找到了有效的 (j,k)。换句话说如果 k 有值的话,那么必然是因为有 j > k,导致的有值。也就是 132 结构中,我们找到了 32,剩下的 i (也就是 132 结构中的 1)则是通过遍历过程中与 k 的比较来找到。这样做的复杂度是 O(n) 的,比树状数组还要快。

class Solution {
public:
    bool find132pattern(vector<int>& nums) {
        stack<int> st;
        int n = nums.size(), k = INT_MIN;
        for(int i = n - 1; i >= 0; i--){
            if(nums[i] < k) return true;
            while(!st.empty() and st.top() < nums[i]) { 
                k = max(k,st.top()); 
                st.pop();
            }
            st.push(nums[i]);
        }
        return false;
    }
};

3.柱状图中的最大矩形
难度:困难
描述:
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。

在这里插入图片描述
示例:
输入: [2,1,5,6,2,3]
输出: 10

思路:
我们需要在柱状图中找出最大的矩形,因此我们可以考虑枚举矩形的宽和高,其中「宽」表示矩形贴着柱状图底边的宽度,「高」表示矩形在柱状图上的高度。

如果我们枚举「宽」,我们可以使用两重循环枚举矩形的左右边界以固定宽度 w,此时矩形的高度 h,就是所有包含在内的柱子的「最小高度」,对应的面积为 w * h。

如果我们枚举「高」,我们可以使用一重循环枚举某一根柱子,将其固定为矩形的高度 h。随后我们从这跟柱子开始向两侧延伸,直到遇到高度小于 h 的柱子,就确定了矩形的左右边界。如果左右边界之间的宽度为 w,那么对应的面积为 w * h。

但如果直接暴力的话肯定是超时的,我们注意到柱体也是高低高问题,那我们如何使用单调栈来解决这个问题呢?

关于这道题网上有很多的讲解,我就不长篇大论了。下面讲其利用单调栈的核心思想。

我们采用枚举【高】的方法。从左往右建立一个单调递增的单调栈,对于枚举【高】,我们就需要找到当前枚举的这个高的右边的第一个低于其高度的柱体和左边第一个低于其高度的柱体。而我们维护了一个从左往右的单调递增的栈,所以需要弹栈的时候就说明右边这个元素的高的低于当前栈顶元素的高的;那对于左边,由于栈是单调递增的,所以其左边一个元素就是那个低于其高的元素。从而我们找到了其左右两边的边界。

这里我们要在前后都插一个0,保证栈内元素都可以找到比其【高】低的左右边界。

int largestRectangleArea(vector<int>& heights)
{
    int ans = 0;
    vector<int> st;
    heights.insert(heights.begin(), 0);//前面补0
    heights.push_back(0);//后面补0
    for (int i = 0; i < heights.size(); i++)
    {
        while (!st.empty() && heights[st.top()] > heights[i])
        {
            int cur = st.top();
            st.pop();
            int left = st.back() + 1;//左边界(比其【高】低的那个柱体是不能取的,所以+1)
            int right = i - 1;//右边界(比其【高】低的那个柱体是不能取的,所以i-1)
            ans = max(ans, (right - left + 1) * heights[cur]);
        }
        st.push_back(i);
    }
    return ans;
}



--------------------------------------------------------------分割线---------------------------------------------------------

四、再来个单调队列?

单调栈都有了,好兄弟单调队列也来一下?

单调队列顾名思义额,和单调栈一样也是分单调递增和单调递减的。
废话不多说直接看题。

描述:
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回滑动窗口中的最大值。

在这里插入图片描述
这道题可以用优先队列或者单调队列来做,优先队列的时间复杂度是O(nlogn),单调队列的时间复杂度是O(n)
//优先队列priorityQueue就是堆,可以实现队列内的自动排序,在c++中默认是大顶堆,java中默认是小顶堆

方法一:优先队列
思路:
对于「最大值」,我们可以想到一种非常合适的数据结构,那就是优先队列(堆),其中的大根堆可以帮助我们实时维护一系列元素中的最大值。

对于本题而言,初始时,我们将数组 nums 的前 k 个元素放入优先队列中。每当我们向右移动窗口时,我们就可以把一个新的元素放入优先队列中,此时堆顶的元素就是堆中所有元素的最大值。然而这个最大值可能并不在滑动窗口中,在这种情况下,我们就判断堆顶元素是不是不在当前窗口内,如果不在我们就移除堆顶元素,直到堆顶元素在窗口内。

在优先队列内,每个元素存储两个值,一个是元素值,一个是元素下标

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        int n = nums.size();
        priority_queue<pair<int, int>> q;
        for (int i = 0; i < k; ++i) {
            q.emplace(nums[i], i);
        }
        vector<int> ans = {q.top().first};
        for (int i = k; i < n; ++i) {
            q.emplace(nums[i], i);
            while (q.top().second <= i - k) {
                q.pop();
            }
            ans.push_back(q.top().first);
        }
        return ans;
    }
};


方法二:单调队列
思路:后续补充吧,没电了溜了

class Solution {
private:
    class MyQueue { //单调队列(从大到小)
    public:
        deque<int> que; // 使用deque来实现单调队列
        // 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
        // 同时pop之前判断队列当前是否为空。
        void pop (int value) {
            if (!que.empty() && value == que.front()) {
                que.pop_front();
            }
        }
        // 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。 
        // 这样就保持了队列里的数值是单调从大到小的了。
        void push (int value) {
            while (!que.empty() && value > que.back()) {
                que.pop_back();
            }
            que.push_back(value);

        }
        // 查询当前队列里的最大值 直接返回队列前端也就是front就可以了。
        int front() {
            return que.front();
        }
    };
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        MyQueue que;
        vector<int> result;
        for (int i = 0; i < k; i++) { // 先将前k的元素放进队列
            que.push(nums[i]);
        }
        result.push_back(que.front()); // result 记录前k的元素的最大值
        for (int i = k; i < nums.size(); i++) {
            que.pop(nums[i - k]); // 滑动窗口移除最前面元素
            que.push(nums[i]); // 滑动窗口前加入最后面的元素
            result.push_back(que.front()); // 记录对应的最大值
        }
        return result;
    }
};
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值