球球速刷LC之数据结构--栈 单调栈 三轮

栈的特性是先入后出,栈的主要题型包括常规栈和单调栈。

常规栈应用

简化路径

使用栈缓存当前到达的路径,遇到"…"弹出栈顶,返回上级目录。注意对最终栈为空处理

    string simplifyPath(string path) {
        if(path.empty()) return "";
        
        string curr_path;
        stack<string>st;
        for(int i=0;i<path.size();++i){
        //跳过 /符号
            if(path[i]=='/'){
                continue;
            }
            //记录当前层级目录名称
            curr_path+=path[i];
            //当前层级目录结束
            if(i+1==path.size ()|| path[i+1]=='/'){
                if(curr_path.empty()==false){
                    if(curr_path==".."){ //返回当前父目录,弹出栈顶
                       if(st.empty()==false) st.pop();                        
                    }else if(curr_path != "."){ //过滤.符号
                        st.push(curr_path);
                    }
                }
                curr_path.clear();
            }
        }
        
        if(st.empty())return "/"; //栈为空代表根目录
        
        string ret;
        while(st.empty()==false){  //将栈内目录名称使用 / 符号连接返回
            ret="/"+st.top()+ret;
            st.pop();
        }
        return ret;
    }
计算波兰表达式

依次遍历表达式,如果是数字,则直接入栈,如果是操作符,则从栈内弹出左右操作数,并进行符号操作后,将操作结果入栈。注意,弹出操作数时,弹出的第一个是右操作数,第二个是左操作数

int do_operator(char c , int lNum,int rNum)
{
    switch(c)
    {
        case '*':return lNum*rNum;
        case '+':return lNum+rNum;
        case '-':return lNum-rNum;
        case '/':return lNum/rNum;
    }
    return 0;
}

bool isOperator(string s)
{
    if(s == "*" || s=="+" ||s == "-" || s=="/") return true;
    return false;
}

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<string> s;
        
        for(int i = 0 ; i < tokens.size() ; ++i)
        {
            if(isOperator(tokens[i]))
            {
                //弹出两个操作数
                int rNum = stoi(s.top());
                s.pop();
                int lNum = stoi(s.top());
                s.pop();
                
                int result = do_operator(tokens[i][0],lNum,rNum);
                s.push(to_string(result));                
                //计算结果
                //将计算结果入栈
            }
            else
            {
                s.push(tokens[i]);
            }
        }
        
        if(!s.empty()) return stoi(s.top());
        
        return 0;
    }
};
使用栈模拟队列
使用队列模拟栈

代码略

去除重复字母333

主要思路是,如果要让最终字符串最小,如果后面的字符b比前面的字符a小,则应该尽可能删除b,把a放到前面。
因此使用栈保存最终结果,遍历原始字符串A依次进栈内:当前字符A与栈顶元素存在3种大小关系
1.如果当前元素A比top 元素小
< a > 栈内已经存在字符A,则跳过当前元素 ,记录的剩余A的数目-1
< b > 栈内没有A存在

              < i > 剩余top元素的数目>0 即A后面还有top存在,弹出top,继续判断A与弹出后栈的新top元素的关系
              < ii > 剩余top元素数目==0.即后面没有top元素,则A入栈 A剩余数目-1,栈内A字符个数+1

2.如果当前元素A>top元素
栈内已有A,跳过,否则A入栈 ;剩余A数目-1

3.当前A == top 元素 ,跳过A 剩余A数目-1;
因此需要记录栈内每种字符的数量,以及剩余字符串内每种字符的数量

class Solution {
public:
    string removeDuplicateLetters(string s) {
        vector<char>st;        
        //入栈前        
        int left_count[26]; //用于记录原是字符串内每种字符数量
        int stack_count[26];//用于记录栈内每种字符的数量
        //初始化为0
        for(int i=0;i<26;++i){
            left_count[i]=0; //用于记录各个字符剩余的数量
            stack_count[i]=0;//用于记录栈内各个字符的数量
        }
        //初始化原始字符串内每种字符数量
        for(auto i:s){
            left_count[i-'a']++; //初始化剩余字符的对应数量
        }
        
        for(int index=0;index<s.size();){
            char a=s[index];
            bool toNext=true;
            //栈为空,直接入栈
            if(st.empty()){
                st.push_back(a);
                //更新当前字符在栈内以及剩余数量
                --left_count[a-'a']; 
                ++stack_count[a-'a'];
            }else{
                auto top = st.back();
                //<1>当前元素与栈顶元素相同,直接跳过,并剩余数量-1
                if(a == top){
                    --left_count[a-'a'];
                  //<2>当前元素大于栈顶元素
                }else if(a>top){ 
                    //栈内不存在当前元素,则该字符入栈,否则直接忽略该字符
                    if(stack_count[a-'a']== 0){
                         st.push_back(a);
                        ++stack_count[a-'a'];
                    }
                    --left_count[a-'a'];
                }else{ //当前元素小于栈顶元素,分类讨论
                    //栈内已有该字符,直接忽略该字符
                    if(stack_count[a-'a']>0){
                      --left_count[a-'a'];
                    }else{
                        //由于把当前元素放到当前top字符前面去,可以减少最终字符串大小
                        //因此如果剩余字符里面还有top,就把当前top弹出
                        //剩余字符里没有top了
                        if(left_count[top-'a'] == 0){
                             st.push_back(a);
                             --left_count[a-'a'];
                             ++stack_count[a-'a'];
                        }else{
                            //剩余字符里还有top,也就是top可以弹出
                            st.pop_back();
                            --stack_count[top-'a'];
                            //注意,此时a继续与下一个top比较,并不是直接入栈
                            toNext=false;
                        }
                    }
                }
            }
            if(toNext)++index;                
        }
        string ret;
        for(auto i:st) ret.push_back(i);
        
        return ret;        
    }
};
检查是否为先序遍历序列333

这个方法简单的说就是利用栈访问过程中不断的砍掉叶子节点。最后看看能不能全部砍掉。只剩下一个NULL,也就是#符号
以例子一为例,:”9,3,4,#,#,1,#,#,2,#,6,#,#” 遇到x # #也就是叶子节点的时候,就把它变为 #
模拟一遍过程:
9,3,4,#,# => 9,3,# 继续读
9,3,#,1,#,# => 9,3,#,# => 9,# 继续读
9,#2,#,6,#,# => 9,#,2,#,# => 9,#,# => #

    bool  isValidSerialization(string preorder) {
        vector<string>st;
        string curr_node;
        for(int i=0;i<preorder.size();++i){
            if(preorder[i]==',') continue; //逗号分割字段
                        
            curr_node+=preorder[i];
            if(i+1==preorder.size() || preorder[i+1]==','){ //当前字段分割完毕
               st.push_back(curr_node);
                //检查是否有叶子节点,有的话持续删除叶子节点   
               while(st.size()>=3 && st[st.size()-1]=="#" && st[st.size()-2]=="#" && st[st.size()-3] !="#"){            
                    st.pop_back();
                    st.pop_back();
                    st[st.size()-1]="#";
              }
              curr_node.clear();
            }
        }        
        if(st.size()==1 && st.back()=="#"){
            return true;
        }
        return false;
    }
};
层次列表迭代器

此题的思路是,如果某列表嵌套多层列表,直到最后一层纯数字列表才能访问,即最深的最先访问到,因此使用栈。此外,对于同一层元素,第一个元素先于最后一个元素访问,因此对于同一层列表需要倒序入栈,即最后一个元素最先入栈。

class NestedIterator {
    stack<NestedInteger>s;
public:
    NestedIterator(vector<NestedInteger> &nestedList) {
        //初始化时将列表倒序入栈
        for(int i=nestedList.size()-1;i>=0;--i){
            s.push(nestedList[i]);
        }
    }

    int _next =0;
    int next() {
        return _next;
    }

    bool hasNext() {
         while(!s.empty()){
            if(s.top().isInteger()){
               //纯数字元素,则直接访问
                int ret = s.top().getInteger();
                s.pop();
                _next = ret;
                return true;
            }else{
                //元素依然为列表,则继续将该列表展开,并倒序入栈
                auto top = s.top();
                s.pop();
                auto nestedList = top.getList();
                for(int i=nestedList.size()-1;i>=0;--i){
                    s.push(nestedList[i]);
                }
            }
        }
        return false;
    }
};

解码字符串

递归是最直接思路


 public String decodeString2(String s) {
        if (s.length() == 0)
            return "";

        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (c >= '0' && c <= '9') {
                // 解析次数
                int digitStart = i++;
                while (s.charAt(i) >= '0' && s.charAt(i) <= '9')
                    i++;
                int num = Integer.parseInt(s.substring(digitStart, i));

                // 找到对应的右括号
                int strStart = i+1; // number must be followed by '['
                int count = 1; 
                i++;
                while (count != 0) {
                    if (s.charAt(i) == '[')
                        count++;
                    else if (s.charAt(i) == ']')
                        count--;
                    i++;
                }
                i--; 

                // 取子字符串
                String subStr = s.substring(strStart, i);

                // 将子字符串解码
                String decodeStr = decodeString(subStr);

                // 将解码的结果拼接到当前的字符串后面
                for (int j = 0; j < num; j++) {
                    sb.append(decodeStr);
                }

            } else {
                // 添加首元素
                sb.append(c);
            }

        }

        return sb.toString();
    }

删除K个数字

类似于删除字符串重复字母,使得最小题目。此处也是使用栈缓存最终数字,如果当前数字i比栈顶数字小,则在删除指标k>0情况下,应该尽量删除当前栈顶元素,使得小数字尽量进位。

class Solution {
public:
    string removeKdigits(string num, int k) {
        string ret;
        
        for(auto c:num){
            while(k && ret.size() && c<ret.back()){//只要还存在删减指标,并且当前数小于前缀末尾,不断删除,从而使得当前小数字c前移
                ret.pop_back();
                --k;
            }
            if(!(ret.empty()&&c=='0'))  ret.push_back(c);//不存在前缀0
        }
        
        //确保所有删除指标用完
        while(ret.size() && (k--)){
            ret.pop_back();
        }
        return ret.empty()?"0":ret;        
    }
};

单调栈

单调栈是在栈的性质上加入新的限制,即保持栈内元素单调增长或单调减少。也可是严格单调增长
或严格单调减少
对于给定栈st,数组A{1 3 5 2 8 1 4 4},假设要求栈从栈底到栈顶保持单调递增,则遍历数组A
栈底部 [ 1 当前元素1 栈为空,直接入栈
栈底部 [1 3 当前元素3大于栈顶元素1,因此栈递增性质不变,3直接入栈
栈底部[1 3 5 当前元素5,>top元素3,递增性质不变,直接入栈
栈底部[1 3 5 : 2 == 当前元素 2 < 5 破坏单调性质,弹出当前栈顶5
== > [1 3 : 2 继续弹出3 == > [1 2
栈底部[1 2 8 当前元素8>栈顶2 直接入栈
栈底部[1 2 8:1 == >[1 2 :1 == > [1:1 == > [1
栈底部[1:4 == >[1 4
栈底部[1 4 : 4 == >[1 4 当前元素==栈元素4,破坏了严格递增条件,因此弹出。
由以上过程可见,单调递增栈,当某元素a最终可以入栈时,此时栈顶元素top是其左边第一个<a的元素。而当a入栈后,由于某一个新的待入栈元素b破坏了当前栈的单调性,使得a需要弹出时,此时b是a在原数组中右边第一个<=于a的元素。
因此,利用单调递增栈,可以找到数组元素a左边第一个<a的元素以及a右边第一个<=a的元素。
由于每个元素只进栈出栈一次,因此时间复杂度为O(n)
对于单调递减栈,则可以找到元素a左边第一个大于a的元素和元素a右边第一个大于等于a的元素。
有时候,需要知道的是元素a距离其左边或右边第一个大于等于a元素的距离,此时栈内可以保存a在原始数组的索引。
总结:
假设 原始数组 {1 3 4 2},使用单调递增栈,当2要入栈时,破坏了单调递增性质
.----------
[ 1 3 4 <–2
.----------
此时首先弹出4,当4被弹出时,我们可以知道4的左边第一个<它的元素是新的栈顶元素3,而右边
第一个<4的元素是当前待入栈元素2.
.--------
[ 1 3 <–2
.--------
继续弹出3,当3被弹出后,我们可以知道3的左边第一个<它的元素是新的栈顶元素1,而右边
第一个<3的元素是当前待入栈元素2.
.--------
[ 1 2
.--------
2达到入栈条件,此时我们知道2的左边第一个<2的元素是当前栈顶元素1.

因此,以栈底在左边的严格单调递增栈为例:
当一个元素达到入栈条件时,我们可求得当前元素a左边第一个<a的元素,即为当前待入栈的栈顶元素
当一个元素被弹出时,我们可以求得当前元素a左端第一个<a的元素和右端第一个<=a的元素。

因此如果我们要求的是左右两端第一个小于或等于a的元素,需要等该元素被弹出时计算。此时为了
保证原始数组内所有元素一定会有被弹出的时候,我们可以在元素数组末尾添加一个比所有元素都小的值。

接雨水333

对于一个能蓄水的凹槽,一定是底部元素a与左右两端第一个>a的元素形成。
因此问题转化为求数组内任意一个元素a左右两端第一个>a的元素aL,aR
,因此可使用单调递增栈。由于是求面积,除了需要知道aL,aR的值,还需要到a的距离,因此栈内保存的是元素的索引。由于当a>右边所有元素时,一定不能形成蓄水凹槽,因此无需a一定被弹出。

class Solution{
public:
  int trap(vector<int>&height){
      if(height.size()<=2) return 0;
      
      stack<int>s;
      int vol=0;
      for(int i=0;i<height.size();++i){
          //当单调递减栈条件被打破时,说明当前top可以与当前top栈内左边第一个元素,以及当前待入栈height[i]形成凹槽 (如果待入栈元素==栈顶元素,形成的凹槽容积为0)
          while(s.empty()==false &&  height[i]>=height[s.top()]){
              int mid=height[s.top()];//当前要弹出元素的左右>自身的位置已经找到
              s.pop();
              if(!s.empty()){
                  vol+= (min(height[i],height[s.top()])-mid)*(i-s.top()-1);
              }
          }
          s.push(i);
      }
      return vol;      
  }
直方图中最大矩形333

依次扫描以每个柱子a为高度所能形成的最大长方形,最终取最大的那个。
对于任何一个柱子a,以a为高度能形成的最大长方形的左边界在a左边第一个<a位置aL向右+1
即aL+1.同理,该长方形的右边界在a右边第一个<a位置aR向右-1.即aR-1.
因此问题转化为:对于任何元素a,找打该元素左右两端第一个<a的位置aL,aR,从而以a为高度
所能形成的最大长方形左右边界位置[aL+1,aR-1]长度为(aR-1)-(aL+1)+1=aR-aL-1
(注:单调递增栈只能找到右边第一个<=a的元素位置,当破坏单调性的元素aR==a时,此时虽然a的右边界可以向aR以及可能aR的更右边拓展。但是由于aR == a,因此a被弹出后,aR依然能计算以a为高度的长方形,且该长方形的右边界>aR,即包含了a以aR-1为右边界形成的长方形,所以最终求出的最大长方形面积不变)
由于要保证每个柱子能形成的最大长方形都能被计算到,因此需要每个元素都能被弹出,因此可以
在数组末尾添加一个比所有元素都小的元素-1.

class Solution {
public:
    int largestRectangleArea(vector<int>& height) {    
        if(height.size() == 0) return 0;
        if(height.size() == 1) return height[0];
        
        //添加一个最小元素,保证原始每一个元素都能被弹出
        height.push_back(-1);
        int max_area = 0;
        stack<int>s;
        for(int i=0; i<height.size();++i){
            //当前元素入栈不改变栈的单调递增性质,直接入栈
            if(s.empty() || height[i] >= height[s.top()]){
                s.push(i);
            }else{
                //当前元素<栈顶元素,破坏了栈顶有序状态
                //需要将栈内不能维持有序状态的元素全部弹出
                //此时被弹出元素的左右第一个小于当前元素的位置可求出                
                while((!s.empty())&& height[s.top()] > height[i]){
                    //求得被弹出元素形成的矩形面积。
                    //矩形高度
                    int h = height[s.top()];
                    s.pop();
                    //矩形的左边界索引 注意为空时的处理
                    int left_index = (s.empty()?-1:s.top())+1;
                    int right_index = i-1;
                    int width = right_index-left_index+1;
                    max_area = max(max_area,width*h);
                }
                //所有不能与当前元素形成有序状态的元素均被弹出,此时可以将当前元素push进入
                s.push(i);
            }
        }
        
        
        return max_area;
    }
            
};
最大矩形

每一行可以看作直方图,从而利用单调栈

class Solution {
public:
    int maxArea(vector<int>&height){
        int area = 0;
        height.push_back(-1);
        stack<int>s;
        for(int i=0;i<height.size();++i){
           while(!s.empty() &&height[s.top()]>=height[i]){
              int h=height[s.top()];
              s.pop();
              int left_border=s.empty()?0:s.top()+1;
              int right_border=i-1;
              int curr_area = h*(right_border-left_border+1);
              area=max(curr_area,area);
           }
           s.push(i);
        }
        height.pop_back();
        return area;
    }
    int maximalRectangle(vector<vector<char>>& matrix) {
        vector<int>heights(matrix[0].size(),0);
        int ret=0;
        for(int i=0;i<matrix.size();++i){
            for(int j=0;j<matrix[0].size();++j){
                if(matrix[i][j]=='0'){
                    heights[j]=0;
                }else{
                    heights[j]++;
                }
            }
            ret=max(ret,maxArea(heights));
        }
        return ret;
    }
};
132模式寻找

此处利用非严格单调递减栈的特殊性质,即对于一个被从非严格单调递减栈中弹出的元素a2,表示在数组a2的后方且入过栈的元素里必然存在一个数字b3>a2.它破坏了栈的单调性,才使得a2被弹出来,记录下这个a2,如果后面再碰到某个未入栈元素c1<a2。则找到了一个数列a2b3c1。 如果从数组的右边向左边遍历并依次入递减栈,则可以找到的形式为
c1b3a2,有c1<a2<b3。即为题意。

class Solution {
public:
    bool find132pattern(vector<int>& nums) {
       if(nums.size()<3) return false;
        int a2 = INT_MIN;
        stack<int>s;
        for(int i=nums.size()-1;i>=0;--i){
            int b3=nums[i];
            while(!s.empty() && b3 > s.top()){
                a2=s.top();
                s.pop();
            }
            s.push(nums[i]);
            int c1=nums[i];
            if(c1 < a2) return true;
        } 
        return false;
        
    }
};
下一个更大元素1333

使用栈底在右边,从数组右边向左遍历的单调递减栈,即可找到每个元素左边第一个>的元素。
此处由于元素不重复,用map暂存。

class Solution {
public:
    vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
        map<int,int>nextGreater;
        stack<int>s;
        for(int i=nums2.size()-1;i>=0;--i){
            while(s.empty()==false && nums2[i]>s.top()){
                s.pop();
            }
            nextGreater[nums2[i]]=s.empty()?-1:s.top();
            s.push(nums2[i]);
        }
        
        vector<int>ret;
        for(auto i:nums1){
            ret.push_back(nextGreater[i]);
        }
        return ret;
        
    }
};
下一个更大元素2

此处的小技巧是将数组翻倍,从而相当于环形展开。
阅读文章:特殊数据结构–单调栈

class Solution {
public:
    //此处将数组翻倍后,将数组翻倍,相当于环形展开
    vector<int> nextGreaterElements(vector<int>& nums) {
        int n=nums.size();
        vector<int>ret(n,0);
        stack<int>s;
        for(int i=2*n-1;i>=0;--i){
            int ii=i%n;
            while(s.empty()==false && nums[ii]>=s.top()){
                s.pop();
            }
            ret[ii]=s.empty()?-1:s.top();
            s.push(nums[ii]);
        }
        return ret;
    }
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值