[力扣刷题总结](栈和单调栈篇)

文章目录

  • ~~~~~~~~~~~~栈~~~~~~~~~~~~
  • 155. 最小栈
    • 解法1:链表
  • 剑指 Offer 31. 栈的压入、弹出序列
    • 解法1:模拟栈
  • 20. 有效的括号
    • 解法1:栈
  • 相似题目: 32. 最长有效括号
    • 解法1:栈
    • 解法2:动态规划
    • 解法3:不需要额外的空间
  • 150. 逆波兰表达式求值
    • 解法1:栈+字符串
  • 相似题目:224. 基本计算器
    • 解法1:栈+字符串
  • 相似题目:227. 基本计算器 II
    • 解法1:栈+字符串
  • 1006. 笨阶乘
    • 解法1:栈+模拟+数学
  • 71. 简化路径
    • 解法1:栈
  • ~~~~~~~~~~~~单调栈~~~~~~~~~~~~
    • 一. 什么是单调栈?
    • 二.什么时候用单调栈?
    • 三. 单调栈的原理是什么?
  • 739. 每日温度
    • 解法1:单调栈
  • 496. 下一个更大元素I
    • 解法1:单调栈+哈希表
  • 503. 下一个更大元素II
    • 解法1:单调栈+循环数组
  • 42.接雨水
    • 解法1:双指针
    • 解法2:单调栈
    • 解法3:动态规划
  • 84.柱状图中最大的矩形
    • 解法1:单调栈
  • 85.最大矩形
    • 解法1:单调栈
  • 相似题目-221.最大正方形
    • 解法1:单调栈
    • 解法2:动态规划
  • 402. 移掉 K 位数字
    • 解法1:贪心+一般方法
    • 解法2:单调栈
  • 316. 去除重复字母
    • 解法1:单调栈
  • 581. 最短无序连续子数组
    • 解法1:双指针+一次扫描
    • 解法2:双指针 + 排序
    • 解法3:单调栈
  • 剑指 Offer 33. 二叉搜索树的后序遍历序列
    • 解法1:递归
    • 解法2:单调栈
  • 1996. 游戏中弱角色的数量
    • 解法1:排序
    • 解法2:排序+单调栈


本文部分参考了 代码随想录

155. 最小栈

力扣链接
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。

实现 MinStack 类:

MinStack() 初始化堆栈对象。
void push(int val) 将元素val推入堆栈。
void pop() 删除堆栈顶部的元素。
int top() 获取堆栈顶部的元素。
int getMin() 获取堆栈中的最小元素。

示例 1:

输入:
[“MinStack”,“push”,“push”,“push”,“getMin”,“pop”,“top”,“getMin”]
[[],[-2],[0],[-3],[],[],[],[]]

输出:
[null,null,null,null,-3,null,0,-2]

解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.

提示:

-231 <= val <= 231 - 1
pop、top 和 getMin 操作总是在 非空栈 上调用
push, pop, top, and getMin最多被调用 3 * 104 次

解法1:链表

无辅助容器

class MinStack {
private:
    struct Node{
        int val;
        int min;
        Node* next;
        Node(int x, int y):val(x),min(y),next(NULL){};
    };
    Node* head;
public:
    MinStack() {
        head = NULL;
    }
    
    void push(int val) {
        if(head == NULL){
            head = new Node(val,val);
        }else{
            int minCur = val < head->min ? val : head->min;
            Node* cur = new Node(val,minCur);
            cur->next = head;
            head = cur;
        }
    }
    
    void pop() {
        head = head->next;
    }
    
    int top() {
        return head->val;
    }
    
    int getMin() {
        return head->min;
    }
};

/**
 * Your MinStack object will be instantiated and called as such:
 * MinStack* obj = new MinStack();
 * obj->push(val);
 * obj->pop();
 * int param_3 = obj->top();
 * int param_4 = obj->getMin();
 */

剑指 Offer 31. 栈的压入、弹出序列

力扣链接
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列。

示例 1:
输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]
输出:true
解释:我们可以按以下顺序执行:
push(1), push(2), push(3), push(4), pop() -> 4,
push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1

示例 2:
输入:pushed = [1,2,3,4,5], popped = [4,3,5,1,2]
输出:false
解释:1 不能在 2 之前弹出。

提示:
0 <= pushed.length == popped.length <= 1000
0 <= pushed[i], popped[i] < 1000
pushed 是 popped 的排列。
注意:本题与主站 946 题相同:https://leetcode-cn.com/problems/validate-stack-sequences/

解法1:模拟栈

思路:
考虑借用一个辅助栈 stack ,模拟 压入 / 弹出操作的排列。根据是否模拟成功,即可得到结果。

入栈操作: 按照压栈序列的顺序执行。
出栈操作: 每次入栈后,循环判断 “栈顶元素 == 弹出序列的当前元素” 是否成立,将符合弹出序列顺序的栈顶元素全部弹出。

代码:

class Solution {
public:
    bool validateStackSequences(vector<int>& pushed, vector<int>& popped) {
        stack<int> ST;
        int ptr=0;//指向出栈列表的指针
        for(int i = 0;i<pushed.size();i++){
            ST.push(pushed[i]);
            while (!ST.empty() && popped[ptr] == ST.top()){
                ST.pop();
                ptr++;
            }
        }
        return ptr == popped.size();
    }
};

复杂度分析:

时间复杂度 O(N) : 其中 N 为列表 pushed 的长度;每个元素最多入栈与出栈一次,即最多共 2N次出入栈操作。
空间复杂度 O(N): 辅助栈 stack 最多同时存储N 个元素。

20. 有效的括号

力扣链接
给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。

示例 1:
输入:s = “()”
输出:true

示例 2:
输入:s = “()[]{}”
输出:true

示例 3:
输入:s = “(]”
输出:false

示例 4:
输入:s = “([)]”
输出:false

示例 5:
输入:s = “{[]}”
输出:true

提示:
1 <= s.length <= 104
s 仅由括号 ‘()[]{}’ 组成

解法1:栈

思路:
括号匹配是使用栈解决的经典问题。 由于栈结构的特殊性,非常适合做对称匹配类的题目。

首先要弄清楚,字符串里的括号不匹配有几种情况。
[力扣刷题总结](栈和单调栈篇)_第1张图片
第一种情况:已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false

第二种情况:遍历字符串匹配的过程中,发现栈里没有要匹配的字符。所以return false

第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号return false

那么什么时候说明左括号和右括号全都匹配了呢,就是字符串遍历完之后,栈是空的,就说明全都匹配了。

但还有一些技巧,在匹配左括号的时候,右括号先入栈,就只需要比较当前元素和栈顶相不相等就可以了,比左括号先入栈代码实现要简单的多了!

代码:

class Solution {
public:
    bool isValid(string s) {
        //1.左多右少 2.右多左少 3.左右不匹配
        stack<int> ST;
        for (int i = 0;i<s.size();i++){
            if (s[i] == '(') ST.push(')');
            else if (s[i] == '[') ST.push(']');
            else if (s[i] == '{') ST.push('}');
            //2.右多左少
            else if(ST.empty()) return false;
            //3.左右不匹配
            else if (s[i] != ST.top()) return false;
            //匹配
            else  ST.pop();
            
        }
        //1.左多右少
        return ST.empty();
        
    }
};

相似题目: 32. 最长有效括号

力扣链接
给你一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

示例 1:

输入:s = “(()”
输出:2
解释:最长有效括号子串是 “()”
示例 2:

输入:s = “)()())”
输出:4
解释:最长有效括号子串是 “()()”
示例 3:

输入:s = “”
输出:0

提示:

0 <= s.length <= 3 * 104
s[i] 为 ‘(’ 或 ‘)’

解法1:栈

思路:
[力扣刷题总结](栈和单调栈篇)_第2张图片

代码:

class Solution {
public:
    int longestValidParentheses(string s) {
        stack<int> sT;
        int res = 0;
        int start = 0;
        for(int i = 0;i<s.size();i++){
            if(s[i] == '(') sT.push(i);
            else{
                if(!sT.empty()){
                    sT.pop();//匹配成功
                    if(sT.empty()) res = max(res,i-start+1);
                    else{
                        res = max(res,i-sT.top());
                    }
                }else{
                    start = i + 1; //更新起点  
                }
            }
        }
        return res;
    }
};

复杂度分析:

时间复杂度: O(n),n 是给定字符串的长度。我们只需要遍历字符串一次即可。

空间复杂度: O(n)。栈的大小在最坏情况下会达到 n,因此空间复杂度为 O(n) 。

解法2:动态规划

思路:
[力扣刷题总结](栈和单调栈篇)_第3张图片[力扣刷题总结](栈和单调栈篇)_第4张图片[力扣刷题总结](栈和单调栈篇)_第5张图片[力扣刷题总结](栈和单调栈篇)_第6张图片
注: 这个在分析时是很容易遗漏的,分析要更细致。我在第一次分析是就遗漏了,提交后,有用例 )()(()))不过,分析后发现是少了这一段。

[力扣刷题总结](栈和单调栈篇)_第7张图片[力扣刷题总结](栈和单调栈篇)_第8张图片[力扣刷题总结](栈和单调栈篇)_第9张图片

代码:

class Solution {
public:
    int longestValidParentheses(string s) {
        int n = s.size();
        vector<int> dp(n,0);
        int result = 0;
        for(int i = 1;i<n;i++){
            if(s[i] == ')'){
                if(s[i-1] == '('){
                    dp[i] = 2;
                    if(i-2>=0){
                        dp[i] = dp[i-2] + 2;
                    }
                }else if(dp[i-1] > 0){//即s[i-1] == ')'
                    if(i-dp[i-1]-1>=0 && s[i-dp[i-1]-1] == '('){
                        dp[i] = dp[i-1] + 2;
                        if(i-dp[i-1]-2>=0){
                            dp[i] = dp[i-1] + dp[i-dp[i-1]-2] + 2;
                        }
                    }
                }
            }
            result = max(result,dp[i]);
        }
        return result;
    }
};

解法3:不需要额外的空间

思路:
[力扣刷题总结](栈和单调栈篇)_第10张图片
代码:

class Solution {
public:
    int longestValidParentheses(string s) {
        int left = 0, right = 0, maxLen = 0;
        int n = s.size();
        for(int i = 0;i<n;i++){
            if(s[i] == '(') left++;
            if(s[i] == ')') right++;
            if(left == right) maxLen = max(maxLen,right*2);
            else if(right > left) right = left = 0;
        }
        left= right= 0;
        for(int i = n-1 ;i>=0;i--){
            if(s[i] == '(') left++;
            if(s[i] == ')') right++;
            if(left == right) maxLen = max(maxLen,right*2);
            else if(right < left) right = left = 0;
        }
        return maxLen;
    }
};

复杂度分析:

时间复杂度: O(n),其中 n 为字符串长度。我们只要正反遍历两边字符串即可。

空间复杂度: O(1)。我们只需要常数空间存放若干变量。

150. 逆波兰表达式求值

力扣链接
根据 逆波兰表示法,求表达式的值。

有效的算符包括 +、-、*、/ 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。

注意 两个整数之间的除法只保留整数部分。

可以保证给定的逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。

示例 1:

输入:tokens = [“2”,“1”,“+”,“3”,“*”]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
示例 2:

输入:tokens = [“4”,“13”,“5”,“/”,“+”]
输出:6
解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
示例 3:

输入:tokens = [“10”,“6”,“9”,“3”,“+”,“-11”,““,”/“,””,“17”,“+”,“5”,“+”]
输出:22
解释:该算式转化为常见的中缀算术表达式为:
((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22

提示:

1 <= tokens.length <= 104
tokens[i] 是一个算符(“+”、“-”、“*” 或 “/”),或是在范围 [-200, 200] 内的一个整数

逆波兰表达式:

逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。

平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。
该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。
逆波兰表达式主要有以下两个优点:

去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中

解法1:栈+字符串

思路:

在1047.删除字符串中的所有相邻重复项 (opens new window)提到了 递归就是用栈来实现的。

所以栈与递归之间在某种程度上是可以转换的! 这一点我们在讲解二叉树的时候,会更详细的讲解到。

那么来看一下本题,其实逆波兰表达式相当于是二叉树中的后序遍历。 大家可以把运算符作为中间节点,按照后序遍历的规则画出一个二叉树。

但我们没有必要从二叉树的角度去解决这个问题,只要知道逆波兰表达式是用后续遍历的方式把二叉树序列化了,就可以了。

在进一步看,本题中每一个子表达式要得出一个结果,然后拿这个结果再进行运算,那么这岂不就是一个相邻字符串消除的过程,和1047.删除字符串中的所有相邻重复项 (opens new window)中的对对碰游戏是不是就非常像了。

相信看完动画大家应该知道,这和1047. 删除字符串中的所有相邻重复项 (opens new window)是差不错的,只不过本题不要相邻元素做消除了,而是做运算!

代码:

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> st;
        for(int i = 0;i<tokens.size();i++){
            if(tokens[i]=="+" || tokens[i]=="-" ||tokens[i]=="*" ||tokens[i]=="/"){
                int num1 = st.top();
                st.pop();
                int num2 = st.top();
                st.pop();
                if(tokens[i]=="+") st.push(num2+num1);
                if(tokens[i]=="-") st.push(num2-num1);
                if(tokens[i]=="*") st.push(num2*num1);
                if(tokens[i]=="/") st.push(num2/num1);
            }else{
                st.push(stoi(tokens[i]));
            }
        }
        int result = st.top();
        st.pop();
        return result;
    }
};

相似题目:224. 基本计算器

给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。

注意:不允许使用任何将字符串作为数学表达式计算的内置函数,比如 eval() 。

示例 1:

输入:s = “1 + 1”
输出:2
示例 2:

输入:s = " 2-1 + 2 "
输出:3
示例 3:

输入:s = “(1+(4+5+2)-3)+(6+8)”
输出:23

提示:

1 <= s.length <= 3 * 105
s 由数字、‘+’、‘-’、‘(’、‘)’、和 ’ ’ 组成
s 表示一个有效的表达式
‘+’ 不能用作一元运算(例如, “+1” 和 “+(2 + 3)” 无效)
‘-’ 可以用作一元运算(即 “-1” 和 “-(2 + 3)” 是有效的)
输入中不存在两个连续的操作符
每个数字和运行的计算将适合于一个有符号的 32位 整数

解法1:栈+字符串

思路:

stack存放左括号前的结果和符号

代码:

class Solution {
public:
    int calculate(string s) {
        int sign = 1;//正负号
        int res = 0;
        stack<int> st;//存放res和sign
        for(int i = 0;i<s.size();i++){
            if(s[i]>= '0' && s[i]<='9'){
                long cur = s[i] - '0';
                while(i+1<s.size() && s[i+1]>= '0' && s[i+1]<='9'){
                    cur = cur * 10 + s[++i] - '0';
                }
                res = res + sign * cur;
            }else if(s[i] == '+') sign = 1;
            else if (s[i] == '-') sign = -1;
            else if (s[i] == '('){
                st.push(res);
                st.push(sign);
                res = 0;
                sign = 1;
            }else if (s[i] == ')'){
                int a = st.top();
                st.pop();
                int b = st.top();
                st.pop();
                res = res*a + b;
            }
        }
        return res;
    }
};

相似题目:227. 基本计算器 II

力扣链接
给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。

整数除法仅保留整数部分。

示例 1:

输入:s = “3+2*2”
输出:7
示例 2:

输入:s = " 3/2 "
输出:1
示例 3:

输入:s = " 3+5 / 2 "
输出:5

提示:

1 <= s.length <= 3 * 105
s 由整数和算符 (‘+’, ‘-’, ‘*’, ‘/’) 组成,中间由一些空格隔开
s 表示一个 有效表达式
表达式中的所有整数都是非负整数,且在范围 [0, 231 - 1] 内
题目数据保证答案是一个 32-bit 整数

解法1:栈+字符串

思路:
[力扣刷题总结](栈和单调栈篇)_第11张图片

[力扣刷题总结](栈和单调栈篇)_第12张图片

代码:

class Solution {
public:
    int calculate(string s) {
        stack<int> numSt;
        char presign = '+';
        long res = 0;
        long cur = 0;
        int size = s.size();
        for(int i = 0;i<size;i++){
            if(s[i]>='0' && s[i] <='9'){
                cur = cur*10 + s[i] - '0';
            }
            if((!(s[i]>='0' && s[i] <='9') && s[i] != ' ') || i == size - 1){
                if(presign == '+'){
                    numSt.push(cur);
                }else if (presign == '-'){
                    numSt.push(-cur);
                }else if (presign == '*' || presign == '/'){
                    int tmp = numSt.top();
                    numSt.pop();
                    numSt.push(presign == '*' ? tmp*cur : tmp / cur);
                }
                presign = s[i];
                cur = 0;
            }
        }
        while(!numSt.empty()){
            res += numSt.top();
            numSt.pop();
        }
        return res;
    }
};

1006. 笨阶乘

力扣链接
通常,正整数 n 的阶乘是所有小于或等于 n 的正整数的乘积。例如,factorial(10) = 10 * 9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1。

相反,我们设计了一个笨阶乘 clumsy:在整数的递减序列中,我们以一个固定顺序的操作符序列来依次替换原有的乘法操作符:乘法(*),除法(/),加法(+)和减法(-)。

例如,clumsy(10) = 10 * 9 / 8 + 7 - 6 * 5 / 4 + 3 - 2 * 1。然而,这些运算仍然使用通常的算术运算顺序:我们在任何加、减步骤之前执行所有的乘法和除法步骤,并且按从左到右处理乘法和除法步骤。

另外,我们使用的除法是地板除法(floor division),所以 10 * 9 / 8 等于 11。这保证结果是一个整数。

实现上面定义的笨函数:给定一个整数 N,它返回 N 的笨阶乘。

示例 1:

输入:4
输出:7
解释:7 = 4 * 3 / 2 + 1
示例 2:

输入:10
输出:12
解释:12 = 10 * 9 / 8 + 7 - 6 * 5 / 4 + 3 - 2 * 1

提示:

1 <= N <= 10000
-2^31 <= answer <= 2^31 - 1 (答案保证符合 32 位整数。)

解法1:栈+模拟+数学

思路:

求解没有括号的中缀表达式的时候,可以用一句顺口溜来概括:遇到乘除立即算,遇到加减先入栈。

代码:

class Solution {
public:
    int clumsy(int n) {
        int op = 0;
        int sum = 0;
        stack<int> st;
        st.push(n);

        for(int i = n-1;i>0;i--){
            if(op == 0){
                int cur = st.top();
                st.pop();
                st.push(cur*i);
            }else if(op == 1){
                int cur = st.top();
                st.pop();
                st.push(cur/i);
            }else if(op == 2){
                st.push(i);
            }else{
                st.push(-i);
            }
            op = (op+1)%4;
        }

        while(!st.empty()){
            int cur = st.top();
            st.pop();
            sum += cur;
        }

        return sum;
    }
};

71. 简化路径

力扣链接
给你一个字符串 path ,表示指向某一文件或目录的 Unix 风格 绝对路径 (以 ‘/’ 开头),请你将其转化为更加简洁的规范路径。

在 Unix 风格的文件系统中,一个点(.)表示当前目录本身;此外,两个点 (…) 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。任意多个连续的斜杠(即,‘//’)都被视为单个斜杠 ‘/’ 。 对于此问题,任何其他格式的点(例如,‘…’)均被视为文件/目录名称。

请注意,返回的 规范路径 必须遵循下述格式:

始终以斜杠 ‘/’ 开头。
两个目录名之间必须只有一个斜杠 ‘/’ 。
最后一个目录名(如果存在)不能 以 ‘/’ 结尾。
此外,路径仅包含从根目录到目标文件或目录的路径上的目录(即,不含 ‘.’ 或 ‘…’)。
返回简化后得到的 规范路径 。

示例 1:

输入:path = “/home/”
输出:“/home”
解释:注意,最后一个目录名后面没有斜杠。
示例 2:

输入:path = “/…/”
输出:“/”
解释:从根目录向上一级是不可行的,因为根目录是你可以到达的最高级。
示例 3:

输入:path = “/home//foo/”
输出:“/home/foo”
解释:在规范路径中,多个连续斜杠需要用一个斜杠替换。
示例 4:

输入:path = “/a/./b/…/…/c/”
输出:“/c”

提示:

1 <= path.length <= 3000
path 由英文字母,数字,‘.’,‘/’ 或 ‘_’ 组成。
path 是一个有效的 Unix 风格绝对路径。

解法1:栈

[力扣刷题总结](栈和单调栈篇)_第13张图片

class Solution {
public:
    string simplifyPath(string path) {
        deque<string> dq;
        string result = "", local = "";
        for(int i = 0;i<=path.size();i++){
            if(i == path.size() || path[i] == '/'){
                if(local != "" && local != "."){
                    if(local == ".."){
                        if(dq.size()) dq.pop_back();
                    }
                    else{
                        dq.push_back(local);
                    }
                }
                local = "";
            }
            else{
                local += path[i];
            }
        }

        while(!dq.empty()){
            result += "/";
            result += dq.front();
            dq.pop_front();
        }
        if(result == "") return "/";

        return result;
    }
};

单调栈

一. 什么是单调栈?

从名字上就听的出来,单调栈中存放的数据应该是有序的,所以单调栈也分为单调递增栈单调递减栈

单调递增栈:单调递增栈就是从栈顶到栈底数据是从小到大
[力扣刷题总结](栈和单调栈篇)_第14张图片

单调递减栈:单调递减栈就是从栈顶到栈底数据是从大到小

二.什么时候用单调栈?

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

三. 单调栈的原理是什么?

为什么时间复杂度是O(n)就可以找到每一个元素的右边第一个比它大的元素位置呢?**
3.1.单调栈的本质是空间换时间,因为在遍历的过程中需要用一个栈来记录右边第一个比当前元素的元素,优点是只需要遍历一次

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

(1)单调栈里存放的元素是什么?
单调栈里只需要存放元素的下标i就可以了,如果需要使用对应的元素,直接T[i]就可以获取。

(2)单调栈里元素是递增呢? 还是递减呢?
注意一下顺序为 从栈头到栈底的顺序,这里我们要使用递增循序(再强调一下是指从栈头到栈底的顺序),因为只有递增的时候,加入一个元素i,才知道栈顶元素在数组中右面第一个比栈顶元素大的元素是i。
(3)使用单调栈主要有三个判断条件。

当前遍历的元素T[i]小于栈顶元素T[st.top()]的情况
当前遍历的元素T[i]等于栈顶元素T[st.top()]的情况
当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况

详细图解见代码随想录

739. 每日温度

[力扣刷题总结](栈和单调栈篇)_第15张图片

解法1:单调栈

分析三种情况:

情况一:当前遍历的元素T[i]小于栈顶元素T[st.top()]的情况
情况二:当前遍历的元素T[i]等于栈顶元素T[st.top()]的情况
情况三:当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况
class Solution {
public:
    vector<int> dailyTemperatures(vector<int>& temperatures) {
        //单调栈 从栈顶到栈底非减
        stack<int> ST;
        vector<int> result(temperatures.size(),0);
        ST.push(0);

        for (int i = 0;i<temperatures.size();i++){
            if (temperatures[i] <= temperatures[ST.top()]){//情况一和情况二
                ST.push(i);
            }else{
                while (!ST.empty() && temperatures[i] > temperatures[ST.top()]){
                    int st_int = ST.top();
                    ST.pop();
                    result[st_int] = i - st_int;
                }
                ST.push(i);
            }
            
        }

        return result;
    }
};

496. 下一个更大元素I

[力扣刷题总结](栈和单调栈篇)_第16张图片
[力扣刷题总结](栈和单调栈篇)_第17张图片

解法1:单调栈+哈希表

思路:

(1)定义这个result数组初始化应该为多少呢?

从题目示例中我们可以看出最后是要求nums1的每个元素在nums2中下一个比当前元素大的元素,那么就要定义一个和nums1一样大小的数组result来存放结果。

题目说如果不存在对应位置就输出 -1 ,所以result数组如果某位置没有被赋值,那么就应该是是-1,所以就初始化为-1。

在遍历nums2的过程中,我们要判断nums2[i]是否在nums1中出现过,因为最后是要根据nums1元素的下标来更新result数组。

(2)注意题目中说是两个没有重复元素 的数组 nums1 和 nums2。

没有重复元素,我们就可以用map来做映射了。根据数值快速找到下标,还可以判断nums2[i]是否在nums1中出现过。
[力扣刷题总结](栈和单调栈篇)_第18张图片
(3)分析三种情况:

本题和739. 每日温度是一样的。栈头到栈底的顺序,要从小到大,也就是保持栈里的元素为递增顺序。

情况一:当前遍历的元素T[i]小于栈顶元素T[st.top()]的情况

此时满足递增栈(栈头到栈底的顺序),所以直接入栈。

情况二:当前遍历的元素T[i]等于栈顶元素T[st.top()]的情况

如果相等的话,依然直接入栈,因为我们要求的是右边第一个比自己大的元素,而不是大于等于!

情况三:当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况

此时如果入栈就不满足递增栈了,这也是找到右边第一个比自己大的元素的时候。

判断栈顶元素是否在nums1里出现过,(注意栈里的元素是nums2的元素),如果出现过,开始记录结果。

class Solution {
public:
    vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
        stack<int> st;
        vector<int> result(nums1.size(), -1);
        if (nums1.size() == 0) return result;

        unordered_map<int, int> umap; // key:下表元素,value:下表
        for (int i = 0; i < nums1.size(); i++) {
            umap[nums1[i]] = i;
        }
        st.push(0);
        for (int i = 1; i < nums2.size(); i++) {
            while (!st.empty() && nums2[i] > nums2[st.top()]) {
                if (umap.count(nums2[st.top()]) > 0) { // 看map里是否存在这个元素
                    int index = umap[nums2[st.top()]]; // 根据map找到nums2[st.top()] 在 nums1中的下表
                    result[index] = nums2[i];
                }
                st.pop();
            }
            st.push(i);
        }
        return result;
    }
};

[力扣刷题总结](栈和单调栈篇)_第19张图片

503. 下一个更大元素II

力扣连接
[力扣刷题总结](栈和单调栈篇)_第20张图片

解法1:单调栈+循环数组

如何实现循环数组?

题目说给出的数组是循环数组,何为循环数组?就是说数组的最后一个元素下一个元素是数组的第一个元素,形状类似于「环」。

(1)一种实现方式是,把数组复制一份到数组末尾,这样虽然不是严格的循环数组,但是对于本题已经足够了,因为本题对数组最多遍历两次。

(2)另一个常见的实现方式是,使用取模运算 %可以把下标 i 映射到数组 nums长度的 0 - N 内

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 = 0;i<2*nums.size();i++){
            // 模拟遍历两边nums,注意一下都是用i % nums.size()来操作
            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;
    }
};

在这里插入图片描述

42.接雨水

力扣链接
[力扣刷题总结](栈和单调栈篇)_第21张图片

解法1:双指针

思路:
[力扣刷题总结](栈和单调栈篇)_第22张图片
(1)首先,如果按照列来计算的话,宽度一定是1了,我们再把每一列的雨水的高度求出来就可以了。

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

(3)
[力扣刷题总结](栈和单调栈篇)_第23张图片
代码:

class Solution {
public:
    int trap(vector<int>& height) {
        int result = 0;
        //双指针
        int left = 0;
        int right = height.size()-1;
        int leftMax = 0;
        int rightMax = 0;

        while(left < right){
            leftMax = max(leftMax,height[left]);
            rightMax = max(rightMax,height[right]);

            if (leftMax < rightMax){
                result += leftMax-height[left];
                left++;
            }else{
                result+=rightMax-height[right];
                right--;
            }

        }
        return result;
    }
};

复杂度分析:

时间复杂度:O(n),其中 n 是数组height 的长度。两个指针的移动总次数不超过 n。
空间复杂度:O(1)。只需要使用常数的额外空间。

解法2:单调栈

思路:

单调栈就是保持栈内元素有序。和栈与队列:单调队列 (opens new window)一样,需要我们自己维持顺序,没有现成的容器可以用。

那么本题使用单调栈有如下几个问题:
(1)首先单调栈是按照行方向来计算雨水,如图:

[力扣刷题总结](栈和单调栈篇)_第24张图片
(2)使用单调栈内元素的顺序是从大到小还是从小到大呢?

从栈头(元素从栈头弹出)到栈底的顺序应该是从小到大的顺序。

因为一旦发现添加的柱子高度大于栈头元素了,此时就出现凹槽了,栈头元素就是凹槽底部的柱子,栈头第二个元素就是凹槽左边的柱子,而添加的元素就是凹槽右边的柱子。
[力扣刷题总结](栈和单调栈篇)_第25张图片
(3)遇到相同高度的柱子怎么办:

遇到相同的元素,更新栈内下标,就是将栈里元素(旧下标)弹出,将新元素(新下标)加入栈中。

例如 5 5 1 3 这种情况。如果添加第二个5的时候就应该将第一个5的下标弹出,把第二个5添加到栈中。
因为我们要求宽度的时候 如果遇到相同高度的柱子,需要使用最右边的柱子来计算宽度。
[力扣刷题总结](栈和单调栈篇)_第26张图片
(4)栈里要保存什么数值

是用单调栈,其实是通过 长 * 宽 来计算雨水面积的。长就是通过柱子的高度来计算,宽是通过柱子之间的下标来计算,

那么栈里有没有必要存一个pair类型的元素,保存柱子的高度和下标呢。
其实不用,栈里就存放int类型的元素就行了,表示下标,想要知道对应的高度,通过height[stack.top()] 就知道弹出的下标对应的高度了。

代码:

class Solution {
public:
    int trap(vector<int>& height) {
        //单调栈
        stack<int> ST;// 存着下标,计算的时候用下标对应的柱子高度
        int result = 0;
        ST.push(0);

        for (int i = 1;i<height.size();i++){
            if(height[i] < height[ST.top()]) ST.push(i);// 情况一
            else if (height[i] == height[ST.top()]){// 情况二
                ST.pop();
                ST.push(i);
            }
            else{
                while(!ST.empty() && height[i] > height[ST.top()]){// 情况三
                    int mid = ST.top();
                    ST.pop();
                    if (!ST.empty()){
                        int h = min(height[i],height[ST.top()])-height[mid];
                        int w = i - ST.top() -1;// 注意减一,只求中间凹槽宽度
                        result += h*w;
                    }
                }
                ST.push(i);
            }
        }
        return result;
    }
};

复杂度分析:

时间复杂度:O(n),其中 n 是数组 height 的长度。从 0 到n−1 的每个下标最多只会入栈和出栈各一次。

空间复杂度:O(n),其中 n 是数组height 的长度。空间复杂度主要取决于栈空间,栈的大小不会超过 n。

解法3:动态规划

思路:
在双指针解法中,我们可以看到只要记录左边柱子的最高高度 和 右边柱子的最高高度,就可以计算当前位置的雨水面积,这就是通过列来计算

当前列雨水面积:min(左边柱子的最高高度,记录右边柱子的最高高度) - 当前柱子高度。
我们把每一个位置的左边最高高度记录在一个数组上(maxLeft),右边最高高度记录在一个数组上(maxRight)。这样就避免了重复计算,这就用到了动态规划。

当前位置,左边的最高高度是前一个位置的左边最高高度和本高度的最大值。

即从左向右遍历:maxLeft[i] = max(height[i], maxLeft[i - 1]);

从右向左遍历:maxRight[i] = max(height[i], maxRight[i + 1]);

这样就找到递推公式。

代码:

class Solution {
public:
    int trap(vector<int>& height) {
        //动态规划
        vector<int> leftMax(height.size(),0);
        vector<int> rightMax(height.size(),0);
        int result = 0;

        // 记录每个柱子左边柱子最大高度
        leftMax[0] = height[0];
        for (int i = 1;i<height.size();i++){
            leftMax[i] = max(leftMax[i-1],height[i]);
        }

        // 记录每个柱子右边柱子最大高度
        rightMax[height.size()-1] = height[height.size()-1];
        for (int i = height.size()-2;i>=0;i--){
            rightMax[i] = max(rightMax[i+1],height[i]);
        }

        for (int i = 0;i<height.size();i++){
            result += min(leftMax[i],rightMax[i]) - height[i];
        }
        return result;
    }
};

复杂性分析:

时间复杂度:O(n)。
存储最大高度数组,需要两次遍历,每次 O(n) 。
最终使用存储的数据更新ans ,O(n)。
空间复杂度:O(n) 额外空间。

84.柱状图中最大的矩形

力扣链接
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
[力扣刷题总结](栈和单调栈篇)_第27张图片

解法1:单调栈

思路:

(1)接雨水 (opens new window)是找每个柱子左右两边第一个大于该柱子高度的柱子,而本题是找每个柱子左右两边第一个小于该柱子的柱子

在题解42. 接雨水 (opens new window)中我讲解了接雨水的单调栈从栈头(元素从栈头弹出)到栈底的顺序应该是从小到大的顺序。那么因为本题是要找每个柱子左右两边第一个小于该柱子的柱子,所以从栈头(元素从栈头弹出)到栈底的顺序应该是从大到小的顺序!

(2)例如:
[力扣刷题总结](栈和单调栈篇)_第28张图片
只有栈里从大到小的顺序,才能保证栈顶元素找到左右两边第一个小于栈顶元素的柱子。所以本题单调栈的顺序正好与接雨水反过来。

此时大家应该可以发现其实就是栈顶和栈顶的下一个元素以及要入栈的三个元素组成了我们要求最大面积的高度和宽度

(3)分析清楚如下三种情况:

情况一:当前遍历的元素heights[i]小于栈顶元素heights[st.top()]的情况
情况二:当前遍历的元素heights[i]等于栈顶元素heights[st.top()]的情况
情况三:当前遍历的元素heights[i]大于栈顶元素heights[st.top()]的情况

[力扣刷题总结](栈和单调栈篇)_第29张图片
对于情况二,遇到相同的元素,更新栈内下标,就是将栈里元素(旧下标)弹出,将新元素(新下标)加入栈中。

例如2 4 4 6这种情况,如果不更新的话其实也不会报错,但是并没有什么意义,只会重复计算!

代码:

class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        //单调栈
        stack<int> ST;
        heights.insert(heights.begin(),0);// 数组头部加入元素0
        heights.push_back(0);// 数组尾部加入元素0
        int result = 0;
        ST.push(0);

        for (int i = 1;i<heights.size();i++){
            if (heights[i] > heights[ST.top()]) ST.push(i);
            else if(heights[i] == heights[ST.top()]){
               // ST.pop();
                ST.push(i);
            }else{
                while(!ST.empty() && heights[i] < heights[ST.top()]){
                    int mid = ST.top();
                    ST.pop();
                    if (!ST.empty()){
                        int w = i-ST.top()-1;
                        result = max(w*heights[mid],result);
                    }
                }
                ST.push(i);
            }
        }
        return result;
    }
};

复杂度分析:

时间复杂度:O(N)。

空间复杂度:O(N)。

85.最大矩形

力扣链接
给定一个仅包含 0 和 1 、大小为 rows x cols 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。

示例 1:
[力扣刷题总结](栈和单调栈篇)_第30张图片
输入:matrix = [[“1”,“0”,“1”,“0”,“0”],[“1”,“0”,“1”,“1”,“1”],[“1”,“1”,“1”,“1”,“1”],[“1”,“0”,“0”,“1”,“0”]]
输出:6
解释:最大矩形如上图所示。

示例 2:
输入:matrix = []
输出:0

示例 3:
输入:matrix = [[“0”]]
输出:0

示例 4:
输入:matrix = [[“1”]]
输出:1

示例 5:
输入:matrix = [[“0”,“0”]]
输出:0

提示:
rows == matrix.length
cols == matrix[0].length
0 <= row, cols <= 200
matrix[i][j] 为 ‘0’ 或 ‘1’

解法1:单调栈

思路:
直接把84题柱状图的代码拿来用了,例如测试用例中,我们将第三行作为最大矩形的底部,则可以得到不同高度柱形图,而该柱形图中的最大矩形刚好为全图的最大矩形。每一行都遍历计算一次就好。

[力扣刷题总结](栈和单调栈篇)_第31张图片
代码:

class Solution {
public:
    int maximalRectangle(vector<vector<char>>& matrix) {
        int n = matrix.size();
        if (n==0) return 0;
        int m = matrix[0].size();
        if (n == 1 && m == 1) return matrix[0][0]-'0';

        vector<int> heights(m,0);
        int result = 0;
        for (int i = 0;i<matrix.size();i++){
            for (int j = 0;j<matrix[0].size();j++){
                if (matrix[i][j]-'0'== 1) heights[j]+=1;
                else heights[j] = 0;
            }
            result = max(result,largestRectangleArea(heights));
        }
        return result;
    }

    int largestRectangleArea(const vector<int>& heights_ori) {
        //单调栈
        stack<int> ST;
        vector<int> heights = heights_ori;
        heights.insert(heights.begin(),0);// 数组头部加入元素0
        heights.push_back(0);// 数组尾部加入元素0
        int result = 0;
        ST.push(0);

        for (int i = 1;i<heights.size();i++){
            if (heights[i] > heights[ST.top()]) ST.push(i);
            else if(heights[i] == heights[ST.top()]){
                ST.pop();
                ST.push(i);
            }else{
                while(!ST.empty() && heights[i] < heights[ST.top()]){
                    int mid = ST.top();
                    ST.pop();
                    if (!ST.empty()){
                        int w = i-ST.top()-1;
                        result = max(w*heights[mid],result);
                    }
                }
                ST.push(i);
            }
        }


        return result;
    }
};

复杂度分析:

时间复杂度:O(mn),其中 m 和 n 分别是矩阵的行数和列数。计算
空间复杂度:O(m)。

相似题目-221.最大正方形

力扣链接
在一个由 ‘0’ 和 ‘1’ 组成的二维矩阵内,找到只包含 ‘1’ 的最大正方形,并返回其面积。

示例 1:
[力扣刷题总结](栈和单调栈篇)_第32张图片

输入:matrix = [[“1”,“0”,“1”,“0”,“0”],[“1”,“0”,“1”,“1”,“1”],[“1”,“1”,“1”,“1”,“1”],[“1”,“0”,“0”,“1”,“0”]]
输出:4

示例 2:
[力扣刷题总结](栈和单调栈篇)_第33张图片

输入:matrix = [[“0”,“1”],[“1”,“0”]]
输出:1

示例 3:
输入:matrix = [[“0”]]
输出:0

提示:
m == matrix.length
n == matrix[i].length
1 <= m, n <= 300
matrix[i][j] 为 ‘0’ 或 ‘1’

解法1:单调栈

思路:
根据上一题改编
代码:

class Solution {
public:
    int maximalSquare(vector<vector<char>>& matrix) {
        int n = matrix.size();
        if (n==0) return 0;
        int m = matrix[0].size();
        if (n == 1 && m == 1) return matrix[0][0]-'0';

        vector<int> heights(m,0);
        int result = 0;
        for (int i = 0;i<matrix.size();i++){
            for (int j = 0;j<matrix[0].size();j++){
                if (matrix[i][j]-'0'== 1) heights[j]+=1;
                else heights[j] = 0;
            }
            result = max(result,largestRectangleArea(heights));
        }
        return result;
    }

    int largestRectangleArea(const vector<int>& heights_ori) {
        //单调栈
        stack<int> ST;
        vector<int> heights = heights_ori;
        heights.insert(heights.begin(),0);// 数组头部加入元素0
        heights.push_back(0);// 数组尾部加入元素0
        int result = 0;
        ST.push(0);

        for (int i = 1;i<heights.size();i++){
            if (heights[i] > heights[ST.top()]) ST.push(i);
            else if(heights[i] == heights[ST.top()]){
                ST.pop();
                ST.push(i);
            }else{
                while(!ST.empty() && heights[i] < heights[ST.top()]){
                    int mid = ST.top();
                    ST.pop();
                    if (!ST.empty()){
                        int w = i-ST.top()-1;
                        w = min(w,heights[mid]);
                        result = max(w*w,result);
                    }
                }
                ST.push(i);
            }
        }


        return result;
    }
};

复杂度分析:

时间复杂度O(m*n)。
空间复杂度O(n)。
[力扣刷题总结](栈和单调栈篇)_第34张图片

解法2:动态规划

思路:

当我们判断以某个点为正方形右下角时最大的正方形时,那它的上方,左方和左上方三个点也一定是某个正方形的右下角,否则该点为右下角的正方形最大就是它自己了。

这是定性的判断,那具体的最大正方形边长呢?我们知道,该点为右下角的正方形的最大边长,最多比它的上方,左方和左上方为右下角的正方形的边长多1,最好的情况是是它的上方,左方和左上方为右下角的正方形的大小都一样的,这样加上该点就可以构成一个更大的正方形。

但如果它的上方,左方和左上方为右下角的正方形的大小不一样,合起来就会缺了某个角落,这时候只能取那三个正方形中最小的正方形的边长加1了。假设dpi表示以i,j为右下角的正方形的最大边长,则有 dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1 当然,如果这个点在原矩阵中本身就是0的话,那dp[i]肯定就是0了。

代码:

class Solution {
public:
    int maximalSquare(vector<vector<char>>& matrix) {
        //动态规划
        int rows = matrix.size();
        int cols = matrix[0].size();
        vector<vector<int>> dp(rows,vector<int>(cols,0));
        int maxEdge = 0;
        
        

        for (int i = 0;i<matrix.size();i++){
            for (int j = 0;j<matrix[0].size();j++){
                if (matrix[i][j] == '1') {
                    if (i == 0 || j == 0){
                        dp[i][j] = 1;
                    }else{
                        dp[i][j] = min(dp[i-1][j-1],min(dp[i][j-1],dp[i-1][j]))+1;
                    }
                    maxEdge = max(dp[i][j],maxEdge);
                }
            }
        }
        return maxEdge*maxEdge;

    }
};

[力扣刷题总结](栈和单调栈篇)_第35张图片
复杂度分析:

时间复杂度:O(mn),其中 m 和 n 是矩阵的行数和列数。需要遍历原始矩阵中的每个元素计算 dp 的值。

空间复杂度:O(mn),其中 m 和 n 是矩阵的行数和列数。创建了一个和原始矩阵大小相同的矩阵 dp。由于状态转移方程中的dp(i,j) 由其上方、左方和左上方的三个相邻位置的dp 值决定,因此可以使用两个一维数组进行状态转移,空间复杂度优化至 O(n)。

402. 移掉 K 位数字

力扣链接
给你一个以字符串表示的非负整数 num 和一个整数 k ,移除这个数中的 k 位数字,使得剩下的数字最小。请你以字符串形式返回这个最小的数字。

示例 1 :
输入:num = “1432219”, k = 3
输出:“1219”
解释:移除掉三个数字 4, 3, 和 2 形成一个新的最小的数字 1219 。

示例 2 :
输入:num = “10200”, k = 1
输出:“200”
解释:移掉首位的 1 剩下的数字为 200. 注意输出不能有任何前导零。

示例 3 :
输入:num = “10”, k = 2
输出:“0”
解释:从原数字移除所有的数字,剩余为空就是 0 。

提示:
1 <= k <= num.length <= 105
num 仅由若干位数字(0 - 9)组成
除了 0 本身之外,num 不含任何前导零

解法1:贪心+一般方法

思路:
从左到右,找第一个比前面大的字符,删除,清零,k次扫描。
代码:

class Solution {
public:
    string removeKdigits(string num, int k) {
        if (num.size() == k) return "0";
        for (int i = 0;i<k;i++){
            int idx = 0;
            for (int j = 1;j<num.size() && num[j] >= num[j-1];j++) {
                idx = j;
            }
            num.erase(num.begin()+idx);
            
        }
        while(num.size()>1 && num[0] == '0') num.erase(num.begin());
        return num;
    }
};

解法2:单调栈

思路:
(1)如果当前遍历的数比栈顶大,符合递增,是满意的,让它入栈。
如果当前遍历的数比栈顶小,栈顶立刻出栈,不管后面有没有更大。

(2)照这么设计,如果是 “0432219”,0 最后肯定被留在栈中,变成 0219,还得再去掉前导0。

能不能直接不让前导 0 入栈?——可以。

加一个判断:栈为空且当前字符为 “0” 时,不入栈。取反,就是入栈的条件:

if c != '0' || len(stack) != 0 {
	stack = append(stack, c) // 入栈
}

(3)还需注意的是,遍历结束时,有可能还没删够 k 个字符,开个循环继续出栈,删低位。

(4)最后如果栈变空了,什么也不剩,则返回 “0”。(这种情况是针对10001这种字符串。)
否则,将栈中剩下的字符,转成字符串返回。

代码:

class Solution {
public:
    string removeKdigits(string num, int k) {
        if (num.size() <= k ) return "0";
        stack<char> ST;

        for(int i = 0;i<num.size();i++){
            
            while(!ST.empty() && ST.top()>num[i] && k>0){
                ST.pop();
                k--;
            }

            if (num[i]!= '0' || !ST.empty()){
                ST.push(num[i]);
            }
        }

        //如果k还是大于0
        while (k>0 && !ST.empty()){
            ST.pop();
            k--;
        }

        string result;
        while (!ST.empty()){
            result += ST.top() ;
            ST.pop();
        }
        reverse(result.begin(),result.end());
        if (result.empty()) return "0";
        return result;
    }
};

[力扣刷题总结](栈和单调栈篇)_第36张图片
复杂度分析:

时间复杂度:O(n)。
空间复杂度:O(n)。栈存储数字需要线性的空间。

316. 去除重复字母

力扣链接
给你一个字符串 s ,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证 返回结果的字典序最小(要求不能打乱其他字符的相对位置)。

注意:该题与 1081 https://leetcode-cn.com/problems/smallest-subsequence-of-distinct-characters 相同

示例 1:
输入:s = “bcabc”
输出:“abc”

示例 2:
输入:s = “cbacdcbc”
输出:“acdb”

提示:
1 <= s.length <= 104
s 由小写英文字母组成

解法1:单调栈

思路:
(1)建立两个表:count表示字符表,uesd用记录字符是否已添加的表
(2)第一遍遍历字符串,用来建立字符表
(3)第二遍遍历字符串,用来生成无重复字符串

代码:

class Solution {
public:
    string removeDuplicateLetters(string s) {
        stack<char> ST;
        //count用于记录每个字母在s中出现的次数
        vector<int> count(26,0);
        for (char c:s) count[c-'a']++;

        vector<bool> used(26,false);
       
        for (char c:s){
            count[c-'a']--;
            if(used[c-'a']) continue;
            //字符c小于s1的尾字符,其尾字符在字符表中还有剩余,所以我们需要删除尾字符,同时标记尾字符为没有使用过
            while(!ST.empty() && c < ST.top() && count[ST.top()-'a']>0){
                used[ST.top()-'a'] = false;
                ST.pop();
            }
            //入栈
            ST.push(c);
            used[c-'a'] = true;
        }
        
        string result;
        while(!ST.empty()){
            result += ST.top();
            ST.pop();
        }
        reverse(result.begin(),result.end());
        return result;
    }
};

复杂度分析:

时间复杂度:O(N),其中 N 为字符串长度。代码中虽然有双重循环,但是每个字符至多只会入栈、出栈各一次。

空间复杂度:O(∣Σ∣),其中 Σ 为字符集合,本题中字符均为小写字母,所以 ∣Σ∣=26。由于栈中的字符不能重复,因此栈中最多只能有∣Σ∣ 个字符,另外需要维护两个数组,分别记录每个字符是否出现在栈中以及每个字符的剩余数量。

class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        int n = heights.size();
        vector<int> left(n,-1), right(n,n);
        stack<int> sT;

        for(int i = 0;i<n;i++){
            while(!sT.empty() && heights[sT.top()] >= heights[i]){
                right[sT.top()] = i;
                sT.pop();
            }
            left[i] = (sT.empty() ? -1 : sT.top());
            sT.push(i);
        }
        int res = 0;
        for(int i = 0;i<n;i++){
            res = max(res,(right[i]-left[i]-1)*heights[i]);
        }
        return res;
    }
};

[力扣刷题总结](栈和单调栈篇)_第37张图片

581. 最短无序连续子数组

力扣链接

给你一个整数数组 nums ,你需要找出一个 连续子数组 ,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。

请你找出符合题意的 最短 子数组,并输出它的长度。

示例 1:
输入:nums = [2,6,4,8,10,9,15]
输出:5
解释:你只需要对 [6, 4, 8, 10, 9] 进行升序排序,那么整个表都会变为升序排序。

示例 2:
输入:nums = [1,2,3,4]
输出:0

示例 3:
输入:nums = [1]
输出:0

提示:
1 <= nums.length <= 104
-105 <= nums[i] <= 105
进阶:你可以设计一个时间复杂度为 O(n) 的解决方案吗?

解法1:双指针+一次扫描

思路:
(1)我们可以假设把这个数组分成三段,左段和右段是标准的升序数组,中段数组虽是无序的,但满足最小值大于左段的最大值,最大值小于右段的最小值。
[力扣刷题总结](栈和单调栈篇)_第38张图片
(2)那么我们目标就很明确了,找中段的左右边界,我们分别定义为begin 和 end;
分两头开始遍历:

从左到右维护一个最大值max,在进入右段之前,那么遍历到的nums[i]都是小于max的,我们要求的end就是遍历中最后一个小于max元素的位置;
同理,从右到左维护一个最小值min,在进入左段之前,那么遍历到的nums[i]也都是大于min的,要求的begin也就是最后一个大于min元素的位置。

代码:

class Solution {
public:
    int findUnsortedSubarray(vector<int>& nums) {
        //双指针+线性扫描
        if (is_sorted(nums.begin(),nums.end())) return 0;
        int right = -1, maxN = INT_MIN;
        int left = 0, minN = INT_MAX;

        for(int i = 0;i<nums.size();i++){
            if(nums[i] < maxN) right = i;
            else maxN= nums[i];

            if (nums[nums.size()-1-i] > minN) left = nums.size()-1-i;
            else minN = nums[nums.size()-1-i];
        }

        return right - left + 1;
    }
};

复杂度分析:

时间复杂度:O(n),其中 n 是给定数组的长度,我们仅需要遍历该数组一次。
时间复杂度:O(1)。我们只需要常数的空间保存若干变量。
[力扣刷题总结](栈和单调栈篇)_第39张图片

解法2:双指针 + 排序

思路:
最终目的是让整个数组有序,那么我们可以先将数组拷贝一份进行排序,然后使用两个指针 i 和 j 分别找到左右两端第一个不同的地方,那么 [i, j]这一区间即是答案。

代码:

class Solution {
public:
    int findUnsortedSubarray(vector<int>& nums) {
        //双指针+排序
        if (is_sorted(nums.begin(),nums.end())) return 0;
        vector<int> nums_(nums);
        sort(nums_.begin(),nums_.end());

        int left = 0;
        while (nums_[left] == nums[left]) left++;
        int right = nums.size()-1;
        while (nums_[right] == nums[right]) right--;

        return right-left +1;
    }
};

复杂度分析:

时间复杂度:O(nlogn)
空间复杂度:O(n)

解法3:单调栈

思路:
用栈来双向遍历一次:
(1)从左到右是升序, 取到最小符合需求的左边界l
(2)从右到左是降序,取到最大符合需求的右边界r

代码:

class Solution {
public:
    int findUnsortedSubarray(vector<int>& nums) {
        stack<int> ST;//单调递增栈
        int left = nums.size();
        int right = 0;

        for (int i = 0;i<nums.size();i++){
            //出栈
            while(!ST.empty() && nums[i] < nums[ST.top()]){
                left = min(left,ST.top());
                ST.pop();
            }
            //入栈
            ST.push(i);
        }

        for (int i = nums.size()-1;i>=0;i--){
            //出栈
            while(!ST.empty() && nums[i] > nums[ST.top()]){
                right = max(right,ST.top());
                ST.pop();
            }
            //入栈
            ST.push(i);
        }

        return right<left?0:right - left + 1;
    }
};

复杂度分析:

时间复杂度:O(n)
空间复杂度:O(n)
[力扣刷题总结](栈和单调栈篇)_第40张图片

剑指 Offer 33. 二叉搜索树的后序遍历序列

力扣链接
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 true,否则返回 false。假设输入的数组的任意两个数字都互不相同。

参考以下这颗二叉搜索树:
在这里插入图片描述

示例 1:
输入: [1,6,3,2,5]
输出: false

示例 2:
输入: [1,3,2,6,5]
输出: true

提示:
数组长度 <= 1000

解法1:递归

思路&代码:

class Solution {
public:
    // 要点:二叉搜索树中根节点的值大于左子树中的任何一个节点的值,小于右子树中任何一个节点的值,子树也是
    bool verifyPostorder(vector<int>& postorder) {
        if (postorder.size() <= 1) return true;
        return traversal(postorder,0,postorder.size()-1);
    }
    // 递归实现
    bool traversal(const vector<int>& postorder,int left,int right){
        if (left >= right) return true;// 当前区域不合法的时候直接返回true就好

        int rootValue = postorder[right];// 当前树的根节点的值

        int k = left;
        // 从当前区域找到第一个大于根节点的,说明后续区域数值都在右子树中
        while(k<right && postorder[k] < rootValue){
            k++;
        }
        // 进行判断后续的区域是否所有的值都是大于当前的根节点,如果出现小于的值就直接返回false
        for (int i = k;i<right;i++){
            if(postorder[i] < rootValue) return false;
        }

        // 当前树没问题就检查左右子树
        if (!traversal(postorder,left,k-1)) return false;
        if (!traversal(postorder,k,right-1)) return false;

        return true;
    }
};

复杂度分析:

时间复杂度为O(n^2)

解法2:单调栈

思路:
[力扣刷题总结](栈和单调栈篇)_第41张图片

遍历数组的所有元素,如果栈为空,就把当前元素压栈。如果栈不为空,并且当前元素大于栈顶元素,说明是升序的,那么就说明当前元素是栈顶元素的右子节点,就把当前元素压栈,如果一直升序,就一直压栈。

如果当前元素小于栈顶元素,说明是倒序的,**说明当前元素是某个节点的左子节点,我们目的是要找到这个左子节点的父节点,**就让栈顶元素出栈,直到栈为空或者栈顶元素小于当前值为止,其中最后一个出栈的就是当前元素的父节点。

代码:

class Solution {
public:
    bool verifyPostorder(vector<int>& postorder) {
        //单调栈 单调递增
        stack<int> ST;

        int rootVal = INT_MAX;
        // 逆向遍历,就是翻转的先序遍历
        for(int i = postorder.size()-1;i>=0;i--){
            //出栈 
            //说明当前节点是前面某个节点的左子节点,我们要找到他的父节点
            while(!ST.empty() && postorder[i] < ST.top()){
                rootVal = ST.top();
                ST.pop();
            }
            //只要遇到了某一个左子节点,才会执行上面的代码,才会更
            //新parent的值,否则parent就是一个非常大的值,也就
            //是说如果一直没有遇到左子节点,那么右子节点可以非常大
            if (postorder[i]>rootVal) return false;
            //入栈
            ST.push(postorder[i]);
        }
        return true;
    }
};

复杂度分析:

每次递归要遍历所有节点,时间复杂度为O(n)。
空间复杂度为O(n)

1996. 游戏中弱角色的数量

力扣链接
你正在参加一个多角色游戏,每个角色都有两个主要属性:攻击 和 防御 。给你一个二维整数数组 properties ,其中 properties[i] = [attacki, defensei] 表示游戏中第 i 个角色的属性。

如果存在一个其他角色的攻击和防御等级 都严格高于 该角色的攻击和防御等级,则认为该角色为 弱角色 。更正式地,如果认为角色 i 弱于 存在的另一个角色 j ,那么 attackj > attacki 且 defensej > defensei 。

返回 弱角色 的数量。

示例 1:

输入:properties = [[5,5],[6,3],[3,6]]
输出:0
解释:不存在攻击和防御都严格高于其他角色的角色。
示例 2:

输入:properties = [[2,2],[3,3]]
输出:1
解释:第一个角色是弱角色,因为第二个角色的攻击和防御严格大于该角色。
示例 3:

输入:properties = [[1,5],[10,4],[4,3]]
输出:1
解释:第三个角色是弱角色,因为第二个角色的攻击和防御严格大于该角色。

提示:

2 <= properties.length <= 105
properties[i].length == 2
1 <= attacki, defensei <= 105

解法1:排序

思路:
按照攻击力降序排序,攻击力相同的按照防御力升序排序;

维护前面已经遍历过的角色的防御力的最大值defense,如果当前角色的防御值小于defense,那么即为弱角色;

不可能出现defense出现在攻击力相同的角色中,因为[攻击力相同的按照防御力升序排序]很好地限制了这种情况;

代码:

class Solution {
public:
    static bool cmp(const vector<int>& a, const vector<int>& b){
        if(a[0] != b[0]) return a[0] >b[0];
        else return a[1] < b[1];
    }
    int numberOfWeakCharacters(vector<vector<int>>& properties) {
        sort(properties.begin(),properties.end(),cmp);
        int result = 0;
        int maxDefense = properties[0][1];

        for(int i = 1;i<properties.size();i++){
            if(maxDefense > properties[i][1]){
                result++;
            }
            maxDefense = max(maxDefense,properties[i][1]);
            
        }
        return result;
    }
};

复杂度分析:

时间复杂度:O(nlogn),其中 nn 为数组的长度。排序的时间复杂度为 O(nlogn),遍历数组的时间为 O(n),总的时间复杂度为 O(nlogn+n)=O(nlogn)。

空间复杂度:O(logn),其中 n为数组的长度。排序时使用的栈空间为 O(logn)。

解法2:排序+单调栈

思路:
对于角色 p,如果我们找到一个角色 q 的防御值与攻击值都严格高于 p 的攻击值和防御值,则我们认为角色 p 为弱角色。

我们联想到使用单调递增栈的解法,单调递增栈中保证栈内所有的元素都按照从小到大进行排列。按照角色攻击值的大小从低到高依次遍历每个元素,使用单调递增栈保存所有角色的防御值,遍历时如果发现栈顶的角色 p 的防御值小于当前的角色 q的防御值,则可以认为找到攻击值和防御值都严格大于 p 的角色 q。

如果所有角色的攻击值都不相同,则上述的单调递增栈的解法比较简单,难点在于如何处理攻击值相同但防御值不同的角色比较问题。我们按照攻击值相同时防御值从大到小进行排序,这样即可保证攻击值相同但防御值不同时的角色在进行比较时不会产生计数。

代码:

class Solution {
public:
    static bool cmp(const vector<int>& a, const vector<int>& b){
        return a[0] == b[0] ? a[1] > b[1] : a[0] < b[0];
    }
    int numberOfWeakCharacters(vector<vector<int>>& properties) {
        sort(properties.begin(),properties.end(),cmp);
        stack<int> ST;
        int result = 0;

        for(auto& p:properties){
            while(!ST.empty() && p[1] > ST.top()){
                result++;
                ST.pop();
            }
            ST.push(p[1]);
        }
        return result;
    }
};

复杂度分析:

时间复杂度:O(nlogn),其中 nn 为数组的长度。排序的时间复杂度为 O(nlogn),然后需要一次遍历的时间为 O(n),总的时间复杂度 O(nlogn+n)=O(nlogn)。

空间复杂度:O(n),其中 n 为数组的长度。需要栈来保存中间变量。

你可能感兴趣的:(数据结构与算法基础,数据结构,算法,memcached)