力扣单调栈相关习题【更新中】

74.每日温度

我怎么能想到用单调栈呢? 什么时候用单调栈呢?

通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。时间复杂度为O(n)。

例如本题其实就是找找到一个元素右边第一个比自己大的元素,此时就应该想到用单调栈了。

那么单调栈的原理是什么呢?为什么时间复杂度是O(n)就可以找到每一个元素的右边第一个比它大的元素位置呢?

单调栈的本质是空间换时间,因为在遍历的过程中需要用一个栈来记录右边第一个比当前元素高的元素,优点是整个数组只需要遍历一次。

更直白来说,就是用一个栈来记录我们遍历过的元素,因为我们遍历数组的时候,我们不知道之前都遍历了哪些元素,以至于遍历一个元素找不到是不是之前遍历过一个更小的,所以我们需要用一个容器(这里用单调栈)来记录我们遍历过的元素。

在使用单调栈的时候首先要明确如下几点:

  1. 单调栈里存放的元素是什么?

单调栈里只需要存放元素的下标i就可以了,如果需要使用对应的元素,直接T[i]就可以获取2

2.单调栈里元素是递增呢? 还是递减呢?

我们可以这样想,每个元素都需要等待一个救星,什么的人能救她呢?就是离她右边最近,且还比她大的元素,如果没找到这样的元素,那么她就需要暂时躲在庇护所(即在单调栈里面存储着),

如果出现的元素越来越小,也就意味着没有救星,那么栈就需要继续存储。因此我们就知道,单调栈的存储顺序是递减的。

于是写出了如下代码,

class Solution {
public:
    vector<int> dailyTemperatures(vector<int>& temperatures) {
        stack<int> st;
        vector<int> res(temperatures.size());
        for(int i=0;i<temperatures.size();i++){
            if(st.empty())st.push(i);
            else if(temperatures[st.top()]>=temperatures[i]){
                st.push(i);
            }
            else if(temperatures[st.top()]<temperatures[i]){//出现了救星
                while(temperatures[st.top()]<temperatures[i]&&!st.empty()){
                    int index=st.top();
                    res[index]=i-index;
                    st.pop();
                }
            }
        }
        return res;
    }
};

但是出现了问题,在于,像这种while循环,st.empty()应该要放在前面去判断!

还有,当单调栈内可以确定Res值的元素被取出来后,不要忘记将当前元素也要入栈!

于是最终代码如下:

class Solution {
public:
    vector<int> dailyTemperatures(vector<int>& temperatures) {
        stack<int> st;
        vector<int> res(temperatures.size());
        for(int i=0;i<temperatures.size();i++){
            if(st.empty())st.push(i);
            else if(temperatures[st.top()]>=temperatures[i]){
                st.push(i);
            }
            else if(temperatures[st.top()]<temperatures[i]){//出现了救星
                while(!st.empty()&&temperatures[st.top()]<temperatures[i]){
                    int index=st.top();
                    res[index]=i-index;
                    st.pop();
                }
                st.push(i);
            }
        }
        return res;
    }
};

判别是否需要使用单调栈,如果需要找到左边或者右边第一个比当前位置的数大或者小,则可以考虑使用单调栈;单调栈的题目如矩形米面积等等

503.下一个更大元素II

这道题和739. 每日温度 也几乎如出一辙。

不过,本题要循环数组了。

本篇我侧重与说一说,如何处理循环数组。

相信不少同学看到这道题,就想那我直接把两个数组拼接在一起,然后使用单调栈求下一个最大值不就行了!

确实可以!

将两个nums数组拼接在一起,使用单调栈计算出每一个元素的下一个最大值,最后再把结果集即result数组resize到原数组大小就可以了。

// 版本一
class Solution {
public:
    vector<int> nextGreaterElements(vector<int>& nums) {
        // 拼接一个新的nums
        vector<int> nums1(nums.begin(), nums.end());
        nums.insert(nums.end(), nums1.begin(), nums1.end());
        // 用新的nums大小来初始化result
        vector<int> result(nums.size(), -1);
        if (nums.size() == 0) return result;

        // 开始单调栈
        stack<int> st;
        st.push(0);
        for (int i = 1; i < nums.size(); i++) { 
            if (nums[i] < nums[st.top()]) st.push(i); 
            else if (nums[i] == nums[st.top()]) st.push(i);
            else { 
                while (!st.empty() && nums[i] > nums[st.top()]) {
                    result[st.top()] = nums[i];
                    st.pop();
                }
                st.push(i);
            }
        }
        // 最后再把结果集即result数组resize到原数组大小
        result.resize(nums.size() / 2);
        return result;
    }
};

这种写法确实比较直观,但做了很多无用操作,例如修改了nums数组,而且最后还要把result数组resize回去。

resize倒是不费时间,是O(1)的操作,但扩充nums数组相当于多了一个O(n)的操作。

其实也可以不扩充nums,而是在遍历的过程中模拟走了两边nums。

代码如下:

// 版本二
class Solution {
public:
    vector<int> nextGreaterElements(vector<int>& nums) {
        vector<int> result(nums.size(), -1);
        if (nums.size() == 0) return result;
        stack<int> st;
        st.push(0);
        for (int i = 1; i < nums.size() * 2; i++) { 
            // 模拟遍历两边nums,注意一下都是用i % nums.size()来操作
            if (nums[i % nums.size()] < nums[st.top()]) st.push(i % nums.size());
            else if (nums[i % nums.size()] == nums[st.top()]) st.push(i % nums.size()); 
            else {
                while (!st.empty() && nums[i % nums.size()] > nums[st.top()]) {
                    result[st.top()] = nums[i % nums.size()];
                    st.pop();
                }
                st.push(i % nums.size());
            }
        }
        return result;
    }
};

Leetcode42.接雨水

暴力解法

本题暴力解法也是也是使用双指针。

首先要明确,要按照行来计算,还是按照列来计算。

按照行来计算如图: 

按照列来计算如图: 

一些同学在实现的时候,很容易一会按照行来计算一会按照列来计算,这样就会越写越乱。

我个人倾向于按照列来计算,比较容易理解,接下来看一下按照列如何计算。

首先,如果按照列来计算的话,宽度一定是1了,我们再把每一列的雨水的高度求出来就可以了。

可以看出每一列雨水的高度,取决于,该列 左侧最高的柱子和右侧最高的柱子中最矮的那个柱子的高度。

这句话可以有点绕,来举一个理解,例如求列4的雨水高度,如图:

列4 左侧最高的柱子是列3,高度为2(以下用lHeight表示)。

列4 右侧最高的柱子是列7,高度为3(以下用rHeight表示)。

列4 柱子的高度为1(以下用height表示)

那么列4的雨水高度为 列3和列7的高度最小值减列4高度,即: min(lHeight, rHeight) - height。

列4的雨水高度求出来了,宽度为1,相乘就是列4的雨水体积了。

此时求出了列4的雨水体积。

一样的方法,只要从头遍历一遍所有的列,然后求出每一列雨水的体积,相加之后就是总雨水的体积了。

首先从头遍历所有的列,并且要注意第一个柱子和最后一个柱子不接雨水,代码如下:

for (int i = 0; i < height.size(); i++) {
    // 第一个柱子和最后一个柱子不接雨水
    if (i == 0 || i == height.size() - 1) continue;
}

在for循环中求左右两边最高柱子,代码如下:

int rHeight = height[i]; // 记录右边柱子的最高高度
int lHeight = height[i]; // 记录左边柱子的最高高度
for (int r = i + 1; r < height.size(); r++) {
    if (height[r] > rHeight) rHeight = height[r];
}
for (int l = i - 1; l >= 0; l--) {
    if (height[l] > lHeight) lHeight = height[l];
}

最后,计算该列的雨水高度,代码如下:

int h = min(lHeight, rHeight) - height[i];
if (h > 0) sum += h; // 注意只有h大于零的时候,在统计到总和中

整体代码如下:

class Solution {
public:
    int trap(vector<int>& height) {
        int sum = 0;
        for (int i = 0; i < height.size(); i++) {
            // 第一个柱子和最后一个柱子不接雨水
            if (i == 0 || i == height.size() - 1) continue;

            int rHeight = height[i]; // 记录右边柱子的最高高度
            int lHeight = height[i]; // 记录左边柱子的最高高度
            for (int r = i + 1; r < height.size(); r++) {
                if (height[r] > rHeight) rHeight = height[r];
            }
            for (int l = i - 1; l >= 0; l--) {
                if (height[l] > lHeight) lHeight = height[l];
            }
            int h = min(lHeight, rHeight) - height[i];
            if (h > 0) sum += h;
        }
        return sum;
    }
};

因为每次遍历列的时候,还要向两边寻找最高的列,所以时间复杂度为O(n^2),空间复杂度为O(1)。

力扣后面修改了后台测试数据,所以以上暴力解法超时了。

思路:从左到右遍历数组,维护一个单调递减的栈,看图很好理解,从左往右遍历时,当高度逐渐减少,此时不会产生水量,如果出现了一个比当前栈中最小的值高的情况:即:

此时我们就需要计算水量,而水量的计算需要几个值,即坑的宽度,瓶颈高度,坑的高度,

然后由瓶颈高度减去坑的高度乘以坑的宽度可得。

为什么 要用瓶颈高度减去坑的高度呢?

从小到大的顺序(从栈头到栈底)。

代码如下:

if (height[i] < height[st.top()])  st.push(i);

如果当前遍历的元素(柱子)高度等于栈顶元素的高度,要跟更新栈顶元素,因为遇到相相同高度的柱子,需要使用最右边的柱子来计算宽度。

代码如下:

if (height[i] == height[st.top()]) { // 例如 5 5 1 7 这种情况
  st.pop();
  st.push(i);
}

如果当前遍历的元素(柱子)高度大于栈顶元素的高度,此时就出现凹槽了,如图所示:

取栈顶元素,将栈顶元素弹出,这个就是凹槽的底部,也就是中间位置,下标记为mid,对应的高度为height[mid](就是图中的高度1)。(是原栈中的top)

pop完一次后,此时的栈顶元素st.top(),就变成了凹槽的左边位置,就是left,下标为st.top(),对应的高度为height[st.top()](就是图中的高度2)。

当前遍历的元素i,就是凹槽右边的位置,下标为i,对应的高度为height[i](就是图中的高度3)。

此时大家应该可以发现其实就是栈顶和栈顶的下一个元素以及要入栈的元素,三个元素来接水!

那么雨水高度是 min(凹槽左边高度, 凹槽右边高度) - 凹槽底部高度,代码为:int h = min(height[st.top()], height[i]) - height[mid];

雨水的宽度是 凹槽右边的下标 - 凹槽左边的下标 - 1(因为只求中间宽度),代码为:int w = i - st.top() - 1 ;

当前凹槽雨水的体积就是:h * w

于是代码如下:

class Solution {
public:
    int trap(vector<int>& height) {
        stack<int> st;
        int sum=0;
        for(int i=0;i<height.size();i++){
            if(st.empty())st.push(i);
            else if(height[st.top()]>=height[i]){
                st.push(i);
            }
            else {
                while(!st.empty()&&height[st.top()]<height[i]){
                    //mid是坑所在的中心,left则是坑的左边,right则是坑的右边
                    int midH=height[st.top()];
                    st.pop();//每次在pop完之后再使用栈之前都需要判断一下是否非空
                    if(!st.empty()){
                        int leftH=height[st.top()];
                        int rightH=height[i];
                        int H=min(leftH,rightH)-midH;
                        int W=i-st.top()-1;
                        sum+=H*W;
                    }

                }
                st.push(i);
            }

        }
        return sum;
        
    }
};

Leetcode84.柱状图中最大的矩形

先来看看本题暴力法:

暴力解法的缺点是在每一个遍历时没有为下一个下标得到什么帮助。

优化思路是空间换时间。

观察下面这个图:

从左往右遍历时,我们以高度来看矩形,当遍历到下标1时,由于下标1的高度小于下标0的高度,则此时以0为高度的矩形,其面积就已经可以确定了

同理,从1遍历到3时,高度不断增加,则此时前面的矩形,其宽度都可以不断延申。

但是当从3到4时,高度下降,那么此时高度为3的矩形,其面积就已经可以确定了

而同理,前面以2号位的高作为的矩形,其面积也可以确定了。因为它比最新的5号位要高。

因此,先确定3号位矩形的面积,然后进而可以确定2号位矩形的面积,观察到这个规律符合先进后出,即符合栈的规律,因此此处可以使用栈,并且我们可以去维护这个单调栈。

因此思路如下:

可以维护一个单调递增的栈,当出现元素不递增时,此即为前面有高度大于当前高度,即面积可以确定了。那么此时将所有大于当前高度的栈元素依次出栈,并且在出栈时就可以计算出面积了。

计算面积时,高度很好表示,高度就是当前height[st.top()]

但问题是宽度呢?

如题所示,此时的宽度:

此时宽度好像看上去是i-st.top(),但事实上不是,我们要意识到,这个矩形也有可能可以往左延申!

但假如是这样的情况:

 

那么此时st.top()==2的情况,不仅可以往右延申,还可以往左延申!

而延申到的位置就是st.pop()之后的新栈顶

 

 新的st.top()就是左边界

因此宽度如下:

此外,在一切都输出完毕后,此时栈中元素可能仍有残留,我们希望能让这部分矩形的面积也要计算,那么我们可以在初始的height结尾添加一个0,这样它会比所有元素都要小,会让前面的所有元素都出栈。

但是另一方面,出栈时要计算该面积,也就是要找到其左边界,但是此时栈的最左边可能已经没有元素了,都为空了,那么就没法有左边界,于是可以自己手动初始在height数组初始左边就添加一个左边界。

因此最终代码如下:

class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        stack<int> st;
        heights.insert(heights.begin(), 0); // 数组头部加入元素0
        heights.push_back(0);
        int maxSquare=0;
        for(int i=0;i<heights.size();i++){
            if(st.empty())st.push(i);
            else if(heights[i]>=heights[st.top()]){
                st.push(i);
            }
            else {
                while(!st.empty()&&heights[i]<heights[st.top()]){
                    int suredIndex=st.top();
                    st.pop();
                    int width=i-st.top()-1;
                    int height=heights[suredIndex];
                    int square=width*height;
                    //cout<<width<<endl;
                    maxSquare=max(maxSquare,square);

                }
                st.push(i);
            }

        }
        
        return maxSquare;
    }
};

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值