栈与单调栈专题

栈与单调栈专题

Hot100 栈专题共 4 题:有效的括号、字符串解码、每日温度、柱状图中最大的矩形。前两题是栈的基础应用,后两题是单调栈的经典场景。单调栈是这个专题的难点,也是竞赛中高频出现的技巧。


一、前置知识:单调栈

普通的栈只保证后进先出,单调栈在此基础上额外维护栈内元素的单调性(单调递增或单调递减)。

单调递增栈:栈底到栈顶单调递增,新元素入栈前,把所有比它大的元素弹出。

单调递减栈:栈底到栈顶单调递减,新元素入栈前,把所有比它小的元素弹出。

单调栈解决的核心问题是:对于数组中的每个元素,找到它左边/右边第一个比它大或比它小的元素 。暴力做法是 O(n2)O(n^2)O(n2),单调栈可以做到 O(n)O(n)O(n)。

复制代码
以找每个元素右边第一个更大元素为例,用单调递减栈:

nums = [2, 1, 5, 3, 4]

遍历到2:栈空,直接入栈,栈=[2]
遍历到1:1<2,不破坏单调性,入栈,栈=[2,1]
遍历到5:5>1,弹出1,记录1的右边第一个更大元素=5
         5>2,弹出2,记录2的右边第一个更大元素=5
         栈空,5入栈,栈=[5]
遍历到3:3<5,入栈,栈=[5,3]
遍历到4:4>3,弹出3,记录3的右边第一个更大元素=4
         4<5,入栈,栈=[5,4]
遍历结束,栈中剩余[5,4],它们右边没有更大元素,答案为-1

结果:2→5, 1→5, 5→-1, 3→4, 4→-1

被弹出的那一刻,就是找到了答案的时机。


二、有效的括号(#20)

题意

给一个只含 (){}[] 的字符串 s,判断括号是否有效(开括号必须按正确顺序闭合)。

复制代码
输入:"()[]{}"  输出:true
输入:"(]"      输出:false
输入:"([)]"    输出:false
输入:"{[]}"    输出:true

思路

用栈维护还未匹配的开括号。

遍历字符串:

  • 遇到开括号 ([{,压入栈
  • 遇到闭括号,检查栈顶是否是对应的开括号:
    • 是:弹出栈顶,继续
    • 否(栈为空或不匹配):直接返回 false

遍历结束后,栈为空说明所有括号都匹配,否则有未闭合的开括号。

复制代码
s = "{[]}"

遍历'{':栈=['{']
遍历'[':栈=['{','[']
遍历']':栈顶'['对应']' ✓,弹出,栈=['{']
遍历'}':栈顶'{'对应'}' ✓,弹出,栈=[]

栈为空,返回true ✓

s = "([)]"

遍历'(':栈=['(']
遍历'[':栈=['(','[']
遍历')':栈顶'['对应']',但当前是')',不匹配 → 返回false ✓

代码

cpp 复制代码
class Solution {
public:
    bool isValid(string s) {
        stack<char> stk;
        for (char c : s) {
            if (c == '(' || c == '[' || c == '{') {
                stk.push(c); // 开括号直接入栈
            } else {
                // 闭括号:栈为空或栈顶不匹配,直接返回false
                if (stk.empty()) return false;
                if (c == ')' && stk.top() != '(') return false;
                if (c == ']' && stk.top() != '[') return false;
                if (c == '}' && stk.top() != '{') return false;
                stk.pop(); // 匹配成功,弹出栈顶
            }
        }
        return stk.empty(); // 栈为空说明全部匹配
    }
};

复杂度

  • 时间 :O(n)O(n)O(n),每个字符入栈出栈各一次
  • 空间 :O(n)O(n)O(n),栈最多存 n/2n/2n/2 个开括号

三、字符串解码(#394)

题意

给编码规则 k[encoded_string],将其解码。k 为正整数,表示 encoded_string 重复 k 次。括号可以嵌套。

复制代码
输入:"3[a]2[bc]"    输出:"aaabcbc"
输入:"3[a2[c]]"     输出:"accaccacc"
输入:"2[abc]3[cd]ef" 输出:"abcabccdcdcdef"

思路

括号可以嵌套,用栈处理嵌套关系。

遍历字符串,维护两个栈:

  • numStack:存数字(重复次数)
  • strStack:存字符串(当前层已构建的内容)

同时维护当前数字 curNum 和当前字符串 curStr

遍历规则:

  • 遇到数字:累积到 curNum(数字可能是多位数)

  • 遇到 [:把 curNumcurStr 分别压入对应的栈,重置为 0 和空串,开始处理内层

  • 遇到 ]:弹出栈顶数字和字符串,把 curStr 重复对应次数后拼接到弹出的字符串后面

  • 遇到字母:直接追加到 curStr

    s = "3[a2[c]]"

    遍历'3':curNum=3
    遍历'[':numStack=[3],strStack=[""],curNum=0,curStr=""
    遍历'a':curStr="a"
    遍历'2':curNum=2
    遍历'[':numStack=[3,2],strStack=["","a"],curNum=0,curStr=""
    遍历'c':curStr="c"
    遍历']':弹出num=2,弹出prevStr="a"
    curStr = "a" + "c"*2 = "acc"
    遍历']':弹出num=3,弹出prevStr=""
    curStr = "" + "acc"*3 = "accaccacc"

    最终:"accaccacc" ✓

注意多位数的处理:遇到连续数字字符时,要用 curNum = curNum * 10 + (c - '0') 累积,而不是直接赋值。

代码

cpp 复制代码
class Solution {
public:
    string decodeString(string s) {
        stack<int> numStack;
        stack<string> strStack;
        string curStr = "";
        int curNum = 0;

        for (char c : s) {
            if (isdigit(c)) {
                curNum = curNum * 10 + (c - '0'); // 处理多位数
            } else if (c == '[') {
                numStack.push(curNum);  // 保存当前重复次数
                strStack.push(curStr);  // 保存当前已构建的字符串
                curNum = 0;             // 重置,准备处理内层
                curStr = "";
            } else if (c == ']') {
                int num = numStack.top(); numStack.pop();
                string prevStr = strStack.top(); strStack.pop();
                string repeated = "";
                for (int i = 0; i < num; i++) repeated += curStr;
                curStr = prevStr + repeated; // 拼接到外层字符串后面
            } else {
                curStr += c; // 普通字母直接追加
            }
        }
        return curStr;
    }
};

复杂度

  • 时间 :O(n⋅k)O(n \cdot k)O(n⋅k),kkk 为最大重复次数,字符串拼接的总长度取决于输出长度
  • 空间 :O(n)O(n)O(n),栈的深度等于括号嵌套层数

四、每日温度(#739)

题意

给一个整数数组 temperatures,表示每天的气温,返回数组 answeranswer[i] 表示第 i 天之后需要等多少天才能等到更高的气温,如果之后不存在更高气温则为 0。

复制代码
输入:[73,74,75,71,69,72,76,73]
输出:[1,1,4,2,1,1,0,0]

第0天73度,第1天74度更高,等1天 → answer[0]=1
第2天75度,第6天76度更高,等4天 → answer[2]=4

思路

单调递减栈,栈里存下标,栈内对应的温度单调递减。

遍历到第 i 天时:

  • 如果当前温度 temperatures[i] 大于栈顶下标对应的温度,说明找到了栈顶那天的"下一个更高温度"

  • 弹出栈顶 janswer[j] = i - j

  • 重复上述操作直到栈为空或栈顶温度 ≥\geq≥ 当前温度

  • 把当前下标 i 入栈

    temperatures = [73,74,75,71,69,72,76,73]

    i=0(73):栈空,入栈,栈=[0]
    i=1(74):74>73,弹出0,answer[0]=1-0=1,栈空,入栈,栈=[1]
    i=2(75):75>74,弹出1,answer[1]=2-1=1,栈空,入栈,栈=[2]
    i=3(71):71<75,入栈,栈=[2,3]
    i=4(69):69<71,入栈,栈=[2,3,4]
    i=5(72):72>69,弹出4,answer[4]=5-4=1
    72>71,弹出3,answer[3]=5-3=2
    72<75,入栈,栈=[2,5]
    i=6(76):76>72,弹出5,answer[5]=6-5=1
    76>75,弹出2,answer[2]=6-2=4
    栈空,入栈,栈=[6]
    i=7(73):73<76,入栈,栈=[6,7]

    遍历结束,栈中剩余[6,7],answer[6]=answer[7]=0(默认值)

    结果:[1,1,4,2,1,1,0,0] ✓

代码

cpp 复制代码
class Solution {
public:
    vector<int> dailyTemperatures(vector<int>& temperatures) {
        int n = temperatures.size();
        vector<int> answer(n, 0); // 默认全为0
        stack<int> stk; // 单调递减栈,存下标

        for (int i = 0; i < n; i++) {
            // 当前温度比栈顶更高,找到了栈顶的答案
            while (!stk.empty() && temperatures[i] > temperatures[stk.top()]) {
                int j = stk.top(); stk.pop();
                answer[j] = i - j;
            }
            stk.push(i); // 当前下标入栈
        }
        return answer;
    }
};

复杂度

  • 时间 :O(n)O(n)O(n),每个下标最多入栈一次、出栈一次
  • 空间 :O(n)O(n)O(n),栈最多存 nnn 个下标

五、柱状图中最大的矩形(#84)

题意

给一个数组 heights,表示柱状图中每根柱子的高度,找出柱状图中能勾勒出来的最大矩形面积。

复制代码
输入:heights = [2,1,5,6,2,3]
输出:10(高度为5和6的两根柱子,宽度为2,面积=10)

  #
# #
# #   #
# # # #
# # # #
# # # # #
2 1 5 6 2 3

思路

对每根柱子,能构成的最大矩形高度就是这根柱子的高度,宽度取决于左边第一根比它矮的柱子右边第一根比它矮的柱子之间的距离。

暴力做法枚举每根柱子,向左右两侧线性扫描找边界,O(n2)O(n^2)O(n2)。用单调递增栈 可以在 O(n)O(n)O(n) 内同时找到每根柱子左右两侧的边界。

单调递增栈的作用 :当一根柱子 i 比栈顶柱子 top 矮时,说明 top 的右边界就是 i,而 top 的左边界就是弹出 top 后新的栈顶(即 top 左边第一根比它矮的柱子)。

哨兵技巧:在数组首尾各加一根高度为 0 的柱子,确保所有柱子都能被弹出(不用单独处理栈中剩余元素),代码更简洁。

复制代码
heights = [2,1,5,6,2,3]
加哨兵后:[0,2,1,5,6,2,3,0](下标0和7是哨兵)

遍历:
i=0(0):栈空,入栈,栈=[0]
i=1(2):2>0,入栈,栈=[0,1]
i=2(1):1<2,弹出1(高度2)
         right=2,left=栈顶0,宽度=2-0-1=1,面积=2*1=2
         1>0,入栈,栈=[0,2]
i=3(5):5>1,入栈,栈=[0,2,3]
i=4(6):6>5,入栈,栈=[0,2,3,4]
i=5(2):2<6,弹出4(高度6)
         right=5,left=栈顶3,宽度=5-3-1=1,面积=6*1=6
         2<5,弹出3(高度5)
         right=5,left=栈顶2,宽度=5-2-1=2,面积=5*2=10 ← 最大
         2>1,入栈,栈=[0,2,5]
i=6(3):3>2,入栈,栈=[0,2,5,6]
i=7(0):0<3,弹出6(高度3)
         right=7,left=栈顶5,宽度=7-5-1=1,面积=3*1=3
         0<2,弹出5(高度2)
         right=7,left=栈顶2,宽度=7-2-1=4,面积=2*4=8
         0<1,弹出2(高度1)
         right=7,left=栈顶0,宽度=7-0-1=6,面积=1*6=6
         栈=[0],0的高度是0,不再弹出

最大面积=10 ✓

面积计算公式:height[top] * (right - left - 1),其中 right 是当前触发弹出的下标,left 是弹出后新栈顶的下标,中间的宽度是 right - left - 1

代码

cpp 复制代码
class Solution {
public:
    int largestRectangleInHistogram(vector<int>& heights) {
        // 首尾各加一个高度为0的哨兵
        heights.insert(heights.begin(), 0);
        heights.push_back(0);

        stack<int> stk; // 单调递增栈,存下标
        int res = 0;

        for (int i = 0; i < heights.size(); i++) {
            // 当前高度小于栈顶高度,弹出并计算面积
            while (!stk.empty() && heights[i] < heights[stk.top()]) {
                int top = stk.top(); stk.pop();
                int left = stk.top();            // 左边界(新栈顶)
                int width = i - left - 1;        // 宽度
                int area = heights[top] * width;
                res = max(res, area);
            }
            stk.push(i);
        }
        return res;
    }
};

复杂度

  • 时间 :O(n)O(n)O(n),每个下标最多入栈一次、出栈一次
  • 空间 :O(n)O(n)O(n),栈 + 哨兵

六、专题小结

题目 数据结构 核心操作 时间 空间
有效的括号 普通栈 遇开括号入栈,遇闭括号检查栈顶 O(n)O(n)O(n) O(n)O(n)O(n)
字符串解码 双栈 [ 时保存现场,] 时恢复并拼接 O(nk)O(nk)O(nk) O(n)O(n)O(n)
每日温度 单调递减栈 温度更高时弹出,差值即为等待天数 O(n)O(n)O(n) O(n)O(n)O(n)
柱状图最大矩形 单调递增栈 高度更低时弹出,左右边界围成矩形 O(n)O(n)O(n) O(n)O(n)O(n)

单调栈的选型规律:

  • 右边第一个更大 的元素 → 单调递减栈(大元素入栈前把小元素弹出,弹出时找到了答案)
  • 右边第一个更小 的元素 → 单调递增栈(小元素入栈前把大元素弹出,弹出时找到了答案)
  • 左边的边界,则在弹出后看新栈顶是什么
相关推荐
code bean1 小时前
【LangChain】少样本提示(Few-Shot Prompting)实战指南
开发语言·python·langchain
心.c1 小时前
RAG文档解析 - pypdf、LlamaParse、DeepDoc、SimpleDirectoryReader到底怎么选?
python·算法·ai
AI科技星1 小时前
基于代数拓扑与等腰梯形素数对网格【乖乖数学】
人工智能·算法·决策树·机器学习·数学建模·数据挖掘·机器人
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第42题】【JVM篇】第2题:JVM内存模型有哪些组成部分?
java·开发语言·jvm·面试
yqcoder1 小时前
深入理解 JavaScript:什么是可迭代对象 (Iterable)?
开发语言·javascript·网络
破阵子443281 小时前
如何用 Claude Code 等 Agent 工具操作 MATLAB(支持代码编写及 Simulink)
开发语言·matlab
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第43题】【JVM篇】第3题:GC分为哪两种?Young GC 和 Full GC有什么区别?
java·开发语言·jvm·后端·面试
jghhh011 小时前
基于时差(TDOA)与 频差(FDOA) 的无源定位
算法
_深海凉_1 小时前
LeetCode热题100-回文链表
算法·leetcode·链表