单调栈算法大总结!!!C++解法,看完这篇文章单调栈嘎嘎乱杀!!!

讲解单调栈

单调栈适用于一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置

以寻找左边第一个小于自己的元素为例:
现有数组a[0,3,4,2,7,5,9]
左侧第一个比自己小的数的答案[-1,0,3,0,2,2,5]

定义stk数组,其作用是在遍历过程中,还有机会成为答案的元素
还是以数组a为例

  1. 遍历第一个数0的时候,
    答案:-1(0左侧没有数字)。
    更新stk:他左侧没有数,他可能成为右侧数的答案,stk=[0]
  2. 遍历第二个数3的时候,
    答案:-1,0(从右向左遍历stk,得到第一个小于3的数0)。
    更新stk:3他比0大,右边是4的话他会成为答案,stk=[0,3],
  3. 遍历第三个数4的时候,
    答案:-1,0,3。(从右向左遍历stk,得到第一个小于4的数3)。
    更新stk:4他比3大,右边是5的话他会成为答案,stk=[0,3,4]
  4. 遍历第四个数2的时候,
    答案-1,0,3,0。(从右向左遍历stk,得到第一个小于2的数0)。
    更新stk:2他比4小,假如后面的数比2大,那么2会是答案,假如后面的数比2小,4也不可能是答案,意味着,当2出现了,左侧比2大的数就都不会成为答案,所以可以将3和4去掉了,stk=[0,2]
  5. 遍历第五个数7的时候,
    答案:-1,0,3,0,2。(从右向左遍历stk,得到第一个小于7的数2)。
    更新stk:7他比2大,右边来个9他就是答案,stk=[0,2,7]
  6. 遍历第六个数5的时候,
    答案-1,0,3,0,2,2。(从右向左遍历stk,得到第一个小于5的数2)。
    更新stk:5比7小,5进入stk了的话,7就不可能成为答案,所以删掉7,入栈5,stk=[0,2,5]
  7. 遍历第七个数9的时候
    答案-1,0,3,0,2,2,5(从右往左遍历stk,得到第一个小于9的数5)
    更新stk,9比5大,后面是10的话9还有机会,所以stk=[0,2,5,9]

stk是个递增数组,单调的,由于只需要在一头进行操作,所以还是个栈,stk就是单调栈

以上讲解对应的代码如下:

左侧第一个小的单调栈

题源acwing830
给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。

#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int stk[N], tt;
int main(){
	int n;
	cin >> n;
	while(n --){
		int x;
		scanf("%d", &x);
		while(tt && stk[tt] >= x) tt--;//如果栈顶元素大于当前遍历元素,就出栈,tt减到底,并且中间元素都不再需要
		if(!tt) printf("-1");//栈顶指针为0了,当前元素左边没有比他小的值
		else printf("%d ", stk[tt]);//输出栈顶元素,栈顶元素就是左侧第一个比它小的
		//注意这里顺序,先输出答案再更新
		stk[++tt] = x;//更新stk,他还有可能成为答案
	}
	return 0}

变形题1:每日温度

题源力扣739

请根据每日 气温 列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。
例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。

提示:气温 列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数

思路

本题是要找右侧第一个比自己大的元素

  1. 单调栈里面放什么:这一题应该放元素下标,这样好计算等待天数,直接temperature[i]也可以获得对应的元素
  2. 单调栈是递增还是递减(栈底到栈顶):应该是递减的,找小的递增,找大的递减

我的学习经验是,先想明白栈里放什么和递增递减的问题,然后再从单调栈使用方向去思考什么时候出栈什么时候入栈,是找左侧还是找右侧是区别很大的,这些想要想明白了再去敲

如何使用这个单调栈:

  • 当遍历温度数组时,对于每个元素,都会检查栈顶元素索引代表的温度是否小于当前元素的温度
  • 如果栈顶元素索引代表的温度小于当前元素的温度,说明已经找到了栈顶元素索引代表的温度右侧第一个大于他的温度,这个时候res[index] = i - stk[s.top()],栈顶索引的右侧第一个大的温度找到了,所以栈顶需要出栈!
  • 如果栈顶元素索引代表的温度并不小于当前元素的温度,说明还没有找到,当前序列是一直递减的,4 3 2 1这样,自然没有右侧第一个大的,所以当前元素的索引入栈

AC代码

class Solution {
public:
    vector<int> dailyTemperatures(vector<int>& temperatures) {
        stack<int> stk;
        // 初始化结果数组,大小与输入相同,所有元素为0
        //气温在这之后都不会升高的话,不用操作
        vector<int> res(temperatures.size(), 0); 
        for (int i = 0; i < temperatures.size(); ++i) {
            while (!stk.empty() && temperatures[stk.top()] < temperatures[i]) {
                // 栈不为空且栈顶元素对应的温度小于当前温度
                int index = stk.top(); // 获取栈顶元素索引
                stk.pop();//栈顶索引对应的温度右侧第一个大的已经找到了,可以出栈
                res[index] = i - index; // 计算两个温度之间的天数差
            }
            stk.push(i); // 将当前索引压入栈
        }
        // 栈中剩余的索引对应的温度都没有更高的温度,它们的结果默认为0,无需额外操作
        return res;
    }
};

变形题2:下一个更大元素I

力扣496
给你两个没有重复元素的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集。
请你找出 nums1 中每个元素在 nums2 中的下一个比其大的值。
nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1 。
在这里插入图片描述

思路

这题和每日温度几乎是一样的,只是绕了一下,需要对单调栈更熟练

  1. 因为是nums1在num2中右侧第一个大的元素,所以需要一个和nums1一样长的数组来存结果,并且由于找不到的话输出-1,所以这个结果数组应该初始化全是-1
  2. 在遍历nums2的过程中,我们要判断nums2[i]是否在nums1中出现过,因为最后是要根据nums1元素的下标来更新result数组。
  3. 注意题目中说是两个没有重复元素的数组 nums1 和 nums2。
  4. 没有重复元素,我们就可以用map来做映射了。根据数值快速找到下标,还可以判断nums2[i]是否在nums1中出现过。

预处理:

unordered_map<int, int> umap; // key:下标元素,value:下标
for (int i = 0; i < nums1.size(); i++) {
    umap[nums1[i]] = i;
}

AC代码

很多题解这里栈也存的索引,其实都可以,毕竟有map了
需要注意的是这里的出栈条件,s.pop()需要放在if语句的外面,需要确保,无论栈顶元素是否在nums1中,都将其从栈中移除,因为栈顶元素的下一个更大的元素已经被找到,只是因为不在nums1中不必被记录

class Solution {
public:
    vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
        stack<int> s;
        vector<int> res(nums1.size(), -1);
        unordered_map<int, int> umap;
        for(int i = 0; i < nums1.size(); i ++) umap[nums1[i]] = i;//记录nums1元素和其索引
        for(int i = 0; i < nums2.size(); i ++){
            while(!s.empty() && nums2[i] > s.top()){
                if(umap.count(s.top()) > 0){
                    res[umap[s.top()]] = nums2[i];
                }
                s.pop();
            }
            s.push(nums2[i]);
        }
        return res;
    }
};

变形题3:下一个更大元素II

给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素),输出每个元素的下一个更大元素。数字 x 的下一个更大的元素是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1。
在这里插入图片描述

思路

除了循环数组,其他是一样的
如何处理循环数组呢?
可以把两个数组拼起来,然后使用单调栈求下一个最大值!最后再把结果即result数组resize到原数组大小就可以了。
但扩充nums数组相当于多了一个O(n)的操作。这里没有必要扩充
nums[i % nums.size()] 代替 nums[i]

AC代码

class Solution {
public:
    vector<int> nextGreaterElements(vector<int>& nums) {
        stack<int> s;
        vector<int> res(nums.size(), -1);
        for(int i = 0; i < nums.size() * 2; i ++){
            while(!s.empty() && nums[i % nums.size()] > nums[s.top()]){
                res[s.top()] = nums[i % nums.size()];
                s.pop();
            }
            s.push(i % nums.size());
        }
        return res;
    }
};

变形题4:接雨水

大厂的热爱
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
在这里插入图片描述
这里给三种方法,暴力,双指针优化和单调栈

思路

暴力法

暴力要先弄明白,是按照行来计算,还是列
必须是以列一列的算,行很难算明白,如果按照列来计算的话,宽度一定是1了,我们再把每一列的雨水的高度求出来就可以了。
每一列的水,取决于,左侧最高柱子和右侧最高柱子中较矮的那个柱子高度
在这里插入图片描述
这个图height = [0,1,0,2,1,0,1,3,2,1,2,1]
列4左侧最高柱子是列3,高度为2;列4右侧最高柱子是列7,高度为3
列4自己高度为1
那么列4的雨水高度为 列3和列7的高度的最小值减列4高度,即: min(lHeight, rHeight) - height。
高度有了,宽度为1,相乘就是列4的雨水体积了。
一样的方法,从头到尾遍历一遍所有的列,求出每一列雨水的体积,相加之后就是总雨水的体积了
但是要注意,第一个柱子和第二个柱子不接雨水:

for(int i = 0; i < height.size(); i ++)//锁定一根柱子
	if(i == 0 || i == height.size() - 1) continue;
}

求两边最高柱子:

int left = height[i]//左最高
int right = height[i];//右最高
for(int l = i - 1; l >= 0; l --) if(height[l] > left) left = height[l];
for(int r = i + 1; r <= height.size() - 1; r ++) if(height[r] > right) right = height[r];

求雨水高度:

int h = min(left, right) - height[i];
if(h > 0) sum += h;

时间复杂度为O(n2),空间复杂度O(1)
暴力法会超时!

双指针

在计算两边柱子最高高度的时候,右很多重复计算,我们把每一个位置的左边最高高度记录在一个数组上(maxLeft),右边最高高度记录在一个数组上(maxRight),这样就避免了重复计算。
当前位置,左边的最高高度是前一个位置的左边最高高度和本高度的最大值。
即从左向右遍历:maxLeft[i] = max(height[i], maxLeft[i - 1]);
从右向左遍历:maxRight[i] = max(height[i], maxRight[i + 1]);

计算左侧最大和右侧最大的时候需要把i这个柱本身考虑进去,因为这样可以统一,如果不考虑进去,会多额外的情况
假设i这个柱本身很高,那么max中就会有他,min之后,一定是i的高度
因为maxLeft和maxRight有两种情况,要么是i柱的高度,要么比i柱更高,无论如何,min之后都是i柱的高度,最后计算水深减去i本身柱高度,会是0

vector<int> maxLeft(height.size(), 0);
vector<int> maxRight(height.size(), 0);

maxLeft[0] = height[0];
for(int i = 1; i < size; i ++) maxLeft[i] = max(height[i], maxLeft[i - 1]);

maxRight[height.size() - 1] = height[height.size() - 1];
for(int i = height.size() - 2; i >= 0; i --) maxRight[i] = max(height[i], maxRight[i + 1]

单调栈

其实在做到双指针的时候应该就能想到单调栈了
平时单调栈记录的是左右第一个比自己大或小的元素
而接雨水这道题目,我们需要寻找一个元素,右边最大元素以及左边最大元素,来计算雨水面积。

需要两个单调栈来分别记录左边最大和右边最大吗?不用的

  1. 首先单调栈是按照行方向来计算雨水的
    在这里插入图片描述

  2. 单调栈内元素是递增还是递减呢
    栈底到栈顶,应该是递减
    因为一旦发现添加的柱子高度大于栈头元素了,此时就出现凹槽了,栈头元素就是凹槽底部的柱子,栈头第二个元素就是凹槽左边的柱子,而添加的元素就是凹槽右边的柱子。
    所以一个栈就解决了一个凹,不需要两个栈!!

  3. 遇到相同高度的柱子了怎么办?
    遇到相同的元素,更新栈内下标,就是将栈里元素(旧下标)弹出,将新元素**(新下标)加入栈中。
    因为我们要求宽度的时候如果遇到相同高度的柱子,需要使用最右边的柱子来计算
    宽度**。实际上这一步并不需要特殊处理

  4. 栈里保存什么值,保存下标,毕竟水的面积是长×宽,

class Solution {
public:
    int trap(vector<int>& height) {
        stack<int> s;
        vector<int> res(height.size(), 0);
        int sum = 0;
        for(int i = 0; i < height.size(); i ++){
            while(!s.empty() && height[s.top()] < height[i]){
                int n = height[s.top()];
                s.pop();
                if(!s.empty()){
                    int h = min(height[s.top()], height[i]) - n;
                    int w = i - s.top() - 1;
                    sum += h * w;
                }
            }
            s.push(i);
        }
        return sum;
    }
};

变形题5:柱状图中的最大矩形

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

思路

暴力法

锁定一根柱子,以此为高,向两边推进
遇到两边第一个小于这跟柱子高度的柱子就break
这样更新每个sum

class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        int sum = 0;
        for (int i = 0; i < heights.size(); i++) {
            int left = i;
            int right = i;
            for (; left >= 0; left--) {
                if (heights[left] < heights[i]) break;
            }
            for (; right < heights.size(); right++) {
                if (heights[right] < heights[i]) break;
            }
            int w = right - left - 1;
            int h = heights[i];
            sum = max(sum, w * h);
        }
        return sum;
    }
};

双指针

这题双指针要记录每根柱子,左边第一个小于该柱子高度的下标,和右边第一个小于该柱子高度的下标。
用for定位每根柱子,用while去寻找左边第一个小于该柱子高度的下标,最后将结果记录在minLeftIndex;相同的方法找右边第一个小于该柱子高度的下标

// 记录每个柱子 左边第一个小于该柱子的下标
minLeftIndex[0] = -1; // 注意这里初始化,防止下面while死循环
for (int i = 1; i < size; i++) {
     int t = i - 1;
     // 这里不是用if,而是不断向左寻找的过程
      while (t >= 0 && heights[t] >= heights[i]) t = minLeftIndex[t];
      minLeftIndex[i] = t;
}
// 记录每个柱子 右边第一个小于该柱子的下标
minRightIndex[size - 1] = size; // 注意这里初始化,防止下面while死循环
for (int i = size - 2; i >= 0; i--) {
    int t = i + 1;
    // 这里不是用if,而是不断向右寻找的过程
    while (t < size && heights[t] >= heights[i]) t = minRightIndex[t];
     minRightIndex[i] = t;
}
//求和
int result = 0;
for (int i = 0; i < size; i++) {
     int sum = heights[i] * (minRightIndex[i] - minLeftIndex[i] - 1);
     result = max(sum, result);
}

单调栈

接雨水是找每根柱子左右两边第一个大于该柱子高度的柱子,而本题是找每个柱子两边第一个小于该柱子高度的柱子
那么单调栈内应该是栈底到栈顶是递增的,遇到小于s.top,则弹出

注意1:
height数组前后需要加上一个元素0,先说为什么结尾加0
为的就是防止height一直升序,假设height数组是[1,2,3,4,5],就会一直入栈,没有计算sum的机会,假如在末尾添加0就可以正常计算了
开头添加0也是一样的,如果数组本身是降序的,[5,4,3,2,1],5入栈后,4与5比较,这时得不到left。因为 将 8 弹出之后,栈里没有元素了,那么为了避免空栈取值,直接跳过了计算结果的逻辑。之后又将6 加入栈(此时8已经弹出了),然后 就是 4 与 栈口元素 8 进行比较,周而复始,那么计算的最后结果resutl就是0。

class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        stack<int> st;
        heights.insert(heights.begin(), 0); // 数组头部加入元素0
        heights.push_back(0); // 数组尾部加入元素0
        st.push(0);
        int result = 0;
        for (int i = 1; i < heights.size(); i++) {
            while (heights[i] < heights[st.top()]) {
                int mid = st.top();
                st.pop();
                int w = i - st.top() - 1;
                int h = heights[mid];
                result = max(result, w * h);
            }
            st.push(i);
        }
        return result;
    }
};
  • 26
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值