Leetcode单调栈总结

数组之间的元素需要比较时,可以考虑单调栈,典型的以空间换取时间的方法:

 因为题目要求,单调栈一般保存的都是索引数组,这点务必注意!

目录

739. 每日温度

84. 柱状图中最大的矩形

42. 接雨水

85. 最大矩形

316. 去除重复字母

402. 移掉K位数字

581. 最短无序连续子数组

 

739. 每日温度

https://leetcode-cn.com/problems/daily-temperatures/

本题题意英文版更好理解 

 

那么我们很容易想到结题方法:

从该点出发,不断往后遍历,找到第一个比自己大的元素,做差,保存,完成

这是典型的暴力解法,代码如下:

class Solution {
public:
    vector<int> dailyTemperatures(vector<int>& T) {
        //双指针循环
        if(T.empty()) return {};
        int size = T.size();
        vector<int>Res;
        int begin = 0,right = begin+1;
        while(begin<size)
        {
            right = begin+1;
            int Temp = T[begin];
            while(right<size)
            {
                if(Temp<T[right]) {Res.push_back(right-begin);break;}
                right++;
            }
            if(right>=size) Res.push_back(0);
            begin++;
        }
        return Res;

    }
};

显然是会超时的 

暴力法进行了很多次的重复操作,这种具有单调性质的题目,我们使用单调栈来完成操作:

 我们现在假设,我们从i开始,要在后续数组中,找到第一个比自己大的数值,并计算二者索引的差值,然后保存,如果没有找到,就是0。

我们完全可以使用一个栈,保持栈内单调递减,也就是说,栈中元素,都没有遇到第一个比自己大的数值,都是等待状态

当遇到一个值大于栈顶元素时,说明栈顶元素已经找到了目标值,做差记录即可,然后弹出,之后重复这个过程

到最后,还留在栈中的元素,说明都没有找到目标值,那么我们全部标记为0即可。

 

我们来看一下图解: 

栈中单调递减,顶部是最小值,下面我们压入72,分别弹出69和71,并让索引做差,得到我们想要的结果 

 

代码完整版本如下: 

class Solution {
public:
    vector<int> dailyTemperatures(vector<int>& T) {
        //双指针循环(超时)
        //单调栈
        if(T.empty()) return {};
        int size = T.size();
        vector<int>Res(size,0);//全部初始化为0
        stack<int>S;
        for(int i = 0;i<size;++i)
        {
            while(!S.empty()&&T[S.top()]<T[i])
            {
                int preIndex = S.top();//栈顶元素索引
                Res[preIndex] = i - preIndex;
                S.pop();//栈顶弹出
            }
            S.push(i);
        }
        return Res;
    }
};

 

84. 柱状图中最大的矩形

https://leetcode-cn.com/problems/largest-rectangle-in-histogram/

暴力解法:

我们从i处,开始向左右两个方向遍历,找比i的值小的或者相等的高度,然后计算面积,这个过程一定要注意细节:

class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        if(heights.empty()) return 0;
        if(heights.size() == 1) return heights[0];
        int sum = 0,MAX = 0;
        for(int i = 0;i<heights.size();++i)
        {
            int left = i-1,right = i+1;
            while(i!=0&&left>=0&&heights[left]>=heights[i]) left--;
            while(i!=heights.size()-1&&right<heights.size()&&heights[right]>=heights[i]) 
            right++;
            sum = heights[i]*(right - left -1);
            MAX = max(MAX,sum);
            cout<<heights[i]<<"  "<<(right - left -1)<<endl;
        }
        return MAX;  
    }
};

显然是超时了:

算法参考:https://leetcode-cn.com/problems/largest-rectangle-in-histogram/solution/zhu-zhuang-tu-zhong-zui-da-de-ju-xing-by-leetcode-/ 

我们之前计算的结果没有为之后服务

我们在暴力法中,选择一个柱子i,然后左右遍历,寻找比i低的柱子,此目的是为了找到长,然后计算面积

我们来看这个遍历过程,不断右移,遇到比i低的柱子,停止,计算面积

那么我们构造一个单点栈,单调递增,栈中的答案都是以i为高的矩形,长度访问内的内容,换句话说,栈中的元素都是可以作为这个矩形长的一部分的。此时栈中单调递增。

那么什么时候这个长会停止呢?就是当遇到一个比目前栈顶(可能不是i)小的元素,那么我们至少需要对栈顶进行操作

因为对于栈顶来说,以自己为高的矩形,长度方向已经遇到了边界,以高度为i的矩形,不能在向右扩散了,因为遇到了边界。

上面的陈述,让我们找到了右边界,下面我们去寻找左边界

当我们将一个元素压入栈中时,

 

左侧如此,右侧也是如此,我们看一下图解:

我们以【6,7,5,2,9】为例,现在将6,7压入栈中

因为5的入栈,以6位高度,和以7位高度的矩形,都找到了自己的右边界,直接出栈

往后的过程不再赘述,都是大同小异,那么我们目前只是确定是右边界,还需要确定左区间

我们反向遍历整个数组,尾部元素压入,然后继续压入内容,直到遇到左侧边界,如图,9遇到了2,需要弹出,并记录左边界

同时我们需要注意,最终留在栈中的内容,我们需要处理,说明他们的左或者右边界是整个数组的两端,但是我们这个过程中,都是采用的左右开区间的方式记录,所以让这些值的边界值为-1或者size本身即可。

这个细节务必注意!

 

完整代码如下:

class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        //两个单调栈的问题
        if(heights.empty()) return 0;
        int size = heights.size();
        vector<int>Right(size,0),Left(size,0);
        stack<int>Temp;
        for(int i = 0;i<size;++i)
        {
            while(Temp.size()&&heights[Temp.top()]>heights[i])//入栈元素小于栈顶元素,需要操作
            {
                Right[Temp.top()] = i;
                Temp.pop();
            }
            Temp.push(i);
        }
        //还有残余,说明右边界是size,左边界同理,设置为-1
        while(Temp.size()) {Right[Temp.top()] = size;Temp.pop();}
        // for(auto item:Right) cout<<item<<endl;
        Temp = stack<int>{};
        for(int i = size-1;i>=0;--i)
        {
            while(Temp.size()&&heights[Temp.top()]>heights[i])//入栈元素小于栈顶元素,需要操作
            {
                Left[Temp.top()] = i;
                Temp.pop();
            }
            Temp.push(i);
        }
        //还有残余,说明右边界是size,左边界同理,设置为-1
        while(Temp.size()) {Left[Temp.top()] = -1;Temp.pop();}
        // for(auto item:Left) cout<<item<<endl;
        //最终计算最大面积
        int MAXSUM = 0;
        for(int i = 0;i<size;++i)
        {
            int Sum = heights[i]*(Right[i] - Left[i] - 1 );
            MAXSUM = max(Sum,MAXSUM);
        }
        return MAXSUM;
        
    }
};

 我们能不能再优化?栈是单调递增的

我们什么时候对栈进行出栈操作?是当目前高度小于栈顶高度的时候,我们需要出栈操作

此时我们找到栈顶的右边界

现在思考两个问题,如果只入栈,要入栈元素是i,栈顶是top,top小于等于i:

考虑两个问题:

1:如果i大于栈顶,那么i的左边界就是目前的栈顶,右边界我们会在循环中计算

2:如果i小于栈顶,会一直让栈顶元素弹出,知道遇到比自己小的,那么此时的栈顶,也是i的左边界

3:如果相等,也会入栈,此时左边界无法描述,但是不会影响面积计算

 

我们对代码进行优化,在计算右边界的时候,就完成左边界的确定

完整代码如下:

class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        //两个单调栈的问题
        if(heights.empty()) return 0;
        int size = heights.size();
        vector<int>Right(size,0),Left(size,0);
        stack<int>Temp;
        for(int i = 0;i<size;++i)
        {
            while(Temp.size()&&heights[Temp.top()]>heights[i])//入栈元素小于栈顶元素,需要操作
            {
                Right[Temp.top()] = i;
                Temp.pop();
            }
            Left[i] = Temp.empty()?-1:Temp.top();
            Temp.push(i);
        }
        //还有残余,说明右边界是size,左边界同理,设置为-1
        while(Temp.size()) {Right[Temp.top()] = size;Temp.pop();}

        //最终计算最大面积
        int MAXSUM = 0;
        for(int i = 0;i<size;++i)
        {
            int Sum = heights[i]*(Right[i] - Left[i] - 1 );
            MAXSUM = max(Sum,MAXSUM);
        }
        return MAXSUM;
        
    }
};

输入数据:

非优化部分:

优化部分:

 可以看到,相同内容的边界处理是有问题的,但是不影响整体,是因为有重复元素总有第一次出现的时候,此时就保证了最大面积。

优化代码如下:可以在初始化左右边界的时候,就直接设置默认值。默认左边界就是-1,默认右边界就是size大小。

class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        if(heights.empty()) return 0;
        int size = heights.size();
        stack<int>S;
        int index = 0;
        vector<int>Left(size,-1),Right(size,size);//在初始化的部分进行改动
        while(index<size){
            while(S.size()&&heights[S.top()]>heights[index]){
                Right[S.top()] = index;
                S.pop();
            }
            if(S.size()) Left[index] = S.top(); 
            S.push(index++);
        }
        int SUM = 0;
        for(int i = 0;i<size;++i){
            int res = heights[i] * (Right[i] - Left[i] - 1);
            SUM = max(SUM,res);
        }
        return SUM;
    }
};

 

42. 接雨水

https://leetcode-cn.com/problems/trapping-rain-water/

有了上面两道题目的铺垫,本题就看到题目也能想到,我们需要用单调栈进行完成

维护一个单调递减的栈,栈中的元素都是可以组成低洼地区的左边界,那么当下一个我们访问的元素高度高于栈顶元素

那么我们就找到了右边界,此时观察能否组成封闭的低洼地带,下面我们看图解:

当(2)3要入栈的时候,此时我们就可以计算一下低洼地区的面积了,因为栈中单调递减,那么显然top的左侧一定比top高

或者相等,要入栈的元素一定比top大,才会进行这部分操作

所以根据三者索引,计算出长度,再根据i和目前栈顶中的最下值作为低洼地区的边界,得到了高,完成计算

关于相等的问题:

完成上图操作后,整个柱状图变成下面的样子

因为不是严格单调递减,相等的情况出现,在红框的范围内,无法形成低洼,因为没有左边界,直到(3)2出栈

s和top如图所示的时候才能形成低洼

整个过程对程序没有什么影响,因为会选择s和i中小的为边界与top做差,相等的这种情况就是让和为0而已:

被计算过的面积也不会被重复计算,因为目标已经出栈,最低点将会在剩余的部分产生,可以理解为计算过的低洼被填平

下面我们来看整段代码:

class Solution {
public:
    int trap(vector<int>& height) {
        //单调栈
        if(height.empty()) return 0;
        stack<int>S;
        int SUM = 0;
        for(int i = 0;i<height.size();++i){
            while(S.size()&&height[i]>height[S.top()]){
                int temp = S.top();S.pop();
                if(S.size() == 0) break;
                int h = min(height[S.top()],height[i]);//确定高的上限
                SUM += (i - S.top() - 1)*(h-height[temp]);//高度和目前的底部做差,找出高是多少
            }
            S.push(i);//压入索引
        }
        return SUM;
    }
};

 

85. 最大矩形

https://leetcode-cn.com/problems/maximal-rectangle/

本题第一眼很像网格dps类型的题目,但是读完题目会发现截然不同

算法参考:https://leetcode-cn.com/problems/maximal-rectangle/solution/zui-da-ju-xing-by-leetcode/

本题实质上和84题很相似,我们可以将本题转化为第84题

我们再看84题:

本题:

联系:

如果我们将本题的数,按列加和,得到结果如图所示,那么就是是第84题了

我们按行加,第一行尾起始,我们计算最大面积,然后第一行累加第二行,当列以0结尾的时候,我们放弃整个列,让其为0

不是0,那么我们累加,效果图如下:

此段程序如下:

        for(int i = 0;i<matrix.size();++i)//每列每列算
        {
            for(int j = 0;j<size;j++)
            {
                dp[j] = matrix[i][j] == '1'?dp[j]+1:0;
            }
            MAX = max(MAX,MAXSUM(dp));
        }

我们对没行的处理和第84题一样,下面程序选择了优化后的版本 

完整代码:

class Solution {
public:
    int maximalRectangle(vector<vector<char>>& matrix) {
        if(matrix.empty()) return 0;
        int MAX = 0;
        int size = matrix[0].size();
        vector<int>dp(size,0);
        for(int i = 0;i<matrix.size();++i)//每列每列算
        {
            for(int j = 0;j<size;j++)
            {
                dp[j] = matrix[i][j] == '1'?dp[j]+1:0;//此处直接将string转换为int类型
            }
            MAX = max(MAX,MAXSUM(dp));
        }
        return MAX; 
    }
    int MAXSUM(vector<int> Num)
    {
        for(auto item:Num) cout<<item<<endl;
        cout<<"item"<<Num.size()<<endl;
        int size = Num.size();
        stack<int>Temp;
        vector<int>Right(size,0),Left(size,0);//记录左右边界
        for(int i = 0;i<size;++i)
        {
            while(Temp.size()&&Num[i] < Num[Temp.top()])//栈中单调递增
            {
                Right[Temp.top()] = i;
                Temp.pop();
            }
            Left[i] = Temp.empty()?-1:Temp.top();
            Temp.push(i);
        }
        //栈中还有剩余元素,我们需要对剩余元素进行处理
        while(Temp.size()) {Right[Temp.top()] = size;Temp.pop();}
        int Res = 0;
        for(int i = 0;i<size;++i) Res = max(Res,(Num[i]*(Right[i] - Left[i] - 1)));
        return Res;
    }
};

优化后的完整版本代码:我们直接将左右边界初始化为-1和size,然后在栈操作的过程中,需要改的进行改。

本题整体意图就是85:找到以i为高的矩形的左右边界

class Solution {
public:
    int maximalRectangle(vector<vector<char>>& matrix) {
        if(matrix.empty()) return 0;
        int row = matrix.size(),col = matrix[0].size();
        int MAX = 0;
        vector<int>Num(matrix[0].size(),0);
        for(int i = 0;i<row;++i){
            if(i == 0) {
                for(int j = 0;j<col;++j) Num[j] = matrix[i][j] - '0';
            }
            else {
                for(int j = 0;j<col;++j){
                    if(matrix[i][j] != '0'){
                        Num[j] += matrix[i][j] - '0';
                    }
                    else Num[j] = 0;
                }
            }
            vector<int> Left(col,-1);
            vector<int> Right(col,col);
            MAX = max(MAX,Calculate(Num,Left,Right));
        }
        return MAX; 
    }
    //找到以i为高的矩形的左右边界
    int Calculate(vector<int> & input,vector<int> & Left,vector<int> & Right){
        stack<int>S;
        int index = 0;
        while(index<input.size()){
            while(S.size()&&input[S.top()]>input[index]){//维护一个单调递增的栈
                Right[S.top()] = index;
                S.pop();
            }
            if(S.size()) Left[index] = S.top();
            S.push(index++);
        }
        int MAX = 0;
        for(int i = 0;i<input.size();++i){
            int res = input[i] * (Right[i] - Left[i] - 1);
            MAX = max(MAX,res);
        }
        return MAX;
    }
};

316. 去除重复字母

https://leetcode-cn.com/problems/remove-duplicate-letters/

class Solution {
public:
    string removeDuplicateLetters(string s) {
        unordered_map<char,int> M;
        for(auto item:s) M[item]++;
        string Res;
        for(auto item:s)
        {
            if(M[item] == 1) Res+=item;
            else M[item]--;
        }
        return Res;
    }
};

https://leetcode-cn.com/problems/remove-duplicate-letters/solution/zhan-by-liweiwei1419/

题目解释:关于字典序

比两个字符串的第一个数字,相等,则比较第二个数字,不等,谁小谁的字典序小,不用往后比较。

 

从简单到难,来找到本题的突破点:

如果输入bca,结果如下:

虽然按照字典序,最优解是abc,但是bc都出现在a前面,且没有在a后面出现,只能是bca

那么如果是bcab,那么我们输出什么?输出:bca,因为b在a后面出现了,目前有两种方案

1:bca

2:cab

显然第一种组合字典序最小

如果输入bcabc,那么最小的字典序就是abc

所以,我们需要知道,某个元素,出现在字符串中的最后位置!

那么我们构建一个栈,压入元素的时候,先判断这个元素的最后位置是不是当前的访问位置

如果是,那么我们没有选择,必须压入栈中

如果不是,那么我们和栈顶元素比大小,将小的放入,(前提是栈顶元素还会在后面出现,否则不能出栈)

构成一个单点递增的栈

难就难在要按照最小的字典序输出

图源及思路来源:https://leetcode-cn.com/problems/remove-duplicate-letters/solution/zhan-by-liweiwei1419/

class Solution {
public:
    string removeDuplicateLetters(string s) {
        if(s.empty()) return "";
        int size = s.size();
        string res = "";
        unordered_map<char,int>Last;
        unordered_map<char,int>used;
        for(int i = 0;i<size;++i) {
            Last[s[i]] = i;
            used[s[i]] = 0;
        }
        stack<char>Temp;
        for(int i = 0;i<size;++i){
            if(used[s[i]] != 0) continue;
            while(Temp.size()&&s[i]<Temp.top()){
                if(Last[Temp.top()] < i) break;
                used[Temp.top()] = 0;
                Temp.pop();
            }
            Temp.push(s[i]);
            used[s[i]] = 1;
        }
        while(Temp.size()) {res += Temp.top();Temp.pop();}
        string rev_str(res.rbegin(),res.rend());
        return rev_str;
    }
};

 

402. 移掉K位数字

https://leetcode-cn.com/problems/remove-k-digits/

思路参考:https://leetcode-cn.com/problems/remove-k-digits/solution/yi-diao-kwei-shu-zi-by-leetcode/

如果一串数字,完全升序递增排列:12345

那么我们拿走一个数字,让剩下数字的组合最小

1:1234

2:2345

3:1345

4:1245

5:1235

显然我们拿走最后一位即可,既可以得到最小的值

下面我们来看例子2:482

我们拿走一个数字,42,82,48,显然拿走8后得到最小值

那么我们可以如此,有一串字符,i,i+1,....;如果i+1小于或者等于i,那么i将会被删除,如果整个数组单调递增,那么直接删除末尾元素

我们需要注意几个细节:

1:当删除完了全部数字,或者只剩下了0,那么我们需要返回“0”,而不是“”;

2:例如10100,删除一个,我们删除的第一个‘1’,那么我们剩下:“0100”,显然我们需要的是剔除第一个无效的0

方法,就是在压入的时候就避免,当栈空,且入栈是0,那么显然是一个无效的前置零,我们直接不进行压入操作

3:当完成删除时,我们需要考虑两个因素,有没有完全删除k个值,k可能还有残留,我们需要从栈的顶部开始进行删除,知道k为0

4:当完成删除时,我们需要考虑,如果是k = 0让循环break,那么我们应该将剩余数组重新压入栈中,保证数据的完整性

 

完整代码如下:

class Solution {
public:
    string removeKdigits(string num, int k) {
        if(k == 0) return num;
        if(num.empty()) return "";
        int size = num.size();
        if(k>=size) return "0";//大于等于都是返回
        stack<char>Temp;
        int Cut = k;
        int i;
        for(i = 0;i<size;++i)
        {
            if(Cut<=0) break;
            while(Cut>0&&Temp.size()&&num[i]<Temp.top())//新的元素小
            {
                Temp.pop();//栈顶弹出
                Cut--;
            }
            if(Temp.empty()&&num[i] == '0') continue;//在插入的阶段就剔除签字零
            Temp.push(num[i]);
        }
        //Cut==0,跳出,此时需要补完整个栈
        while(i<size) Temp.push(num[i++]);
        //情况1:单调递增,Cut一个都没有删除
        //情况2:Cut还有剩余,说明还要删除元素,从尾部删除
        while(Cut>0) {Temp.pop();Cut--;}//从尾部弹出元素

        //反转及拷贝内容:
        vector<int>Stemp;
        while(Temp.size()){Stemp.push_back(Temp.top());Temp.pop();}

        string Res;
        for(int i = Stemp.size()-1;i>=0;--i) Res+=Stemp[i];
        return Res == ""?"0":Res;//如果全部删除或者10,删除了1,那么Res是空,此时我们需要返回0
    }
};

 

 

581. 最短无序连续子数组

https://leetcode-cn.com/problems/shortest-unsorted-continuous-subarray/

 

 

 

 

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
引用\[1\]提供了一个朴素的解法,使用两个栈来存储字符串,一个栈用来存储普通字符,另一个栈用来存储特殊字符。遍历字符串,如果是普通字符则压入第一个栈,如果是特殊字符则弹出第一个栈的栈顶元素。最后比较两个栈是否相同即可判断字符串是否有效。这个解法的时间复杂度是O(M + N),空间复杂度也是O(M + N)。\[1\] 引用\[2\]提供了另一个栈的应用场景,即判断括号是否有效。遍历字符串,如果是左括号则压入栈,如果是右括号则判断和栈顶元素是否匹配,不匹配则返回false,匹配则弹出栈顶元素。最后判断栈是否为空即可判断括号是否有效。\[2\] 引用\[3\]也提供了一个判断括号是否有效的解法,使用栈来操作。遍历字符串,如果是左括号则压入栈,如果是右括号则判断和栈顶元素是否匹配,不匹配则返回false,匹配则弹出栈顶元素。最后判断栈是否为空即可判断括号是否有效。这个解法使用了HashMap来存储括号的对应关系。\[3\] 综上所述,栈在解决字符串相关问题中有着广泛的应用,包括判断字符串是否有效、逆波兰表达式等。在解决这些问题时,栈可以帮助我们保存和处理字符的顺序,从而简化问题的处理过程。 #### 引用[.reference_title] - *1* *3* [Leetcode刷题03-栈](https://blog.csdn.net/weixin_47802917/article/details/123007699)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v4^insert_chatgpt"}} ] [.reference_item] - *2* [leetCode-栈类型详解](https://blog.csdn.net/zhiyikeji/article/details/125508011)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v4^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值