算法

栈、队列 & 单调结构

LIFO/FIFO 性质与单调性不变量,消除冗余比较,O(n) 解决区间极值与括号匹配

概览

栈和队列的本质是约束访问顺序:栈只能操作顶部(LIFO),队列只能操作两端(FIFO)。单调栈/单调双端队列在此基础上额外维护一个单调性不变量——新元素进入时弹出破坏单调性的旧元素,保证每个元素最多入栈出栈各一次,从而将区间极值问题从 O(n²) 降到 O(n)。

四种结构速查

stack<T>LIFO,只操作 top;括号匹配、递归模拟、单调栈基础
queue<T>FIFO,BFS 层序遍历;front() 读头,back() 读尾
deque<T>双端队列,两端都可 push/pop;单调双端队列的底层
priority_queue<T>堆(默认最大堆);top-K、Dijkstra;详见 heap/ 章节

单调栈核心思路

维护不变量:栈中元素从栈底到栈顶单调(递增或递减)。当新元素 x 进入时,弹出所有不满足单调性的栈顶,弹出时即可确定被弹元素的「答案」(下一个更大/更小元素)。每个元素只进出一次,整体 O(n)。

⚠️ pop() 无返回值

C++ 的 stack::pop() / queue::pop() 不返回值!读值必须先 top()/front(),再 pop()。

// ❌ Wrong — pop() returns void
int x = st.pop();

// ✅ Correct
int x = st.top();
st.pop();

复杂度

时间 O(n)每个元素最多入栈一次、出栈一次,总操作 O(2n)
空间 O(n)栈/队列最坏存储 n 个元素

栈:括号匹配

括号匹配是栈最经典的应用:遇到左括号压栈,遇到右括号弹栈检查是否配对,最后栈空则合法。

核心思想

左括号入栈;右括号时若栈空或栈顶不匹配,则非法;匹配则弹栈。扫描完毕后栈若非空(有未配对的左括号)也非法。

步骤

  1. 遍历字符串每个字符 c
  2. 若 c 是左括号('(' / '[' / '{'),压栈
  3. 若 c 是右括号,判断栈是否为空(为空则非法)
  4. 弹出栈顶,检查是否与 c 配对,不配对则非法
  5. 遍历结束,栈空则合法,否则非法

C++ 实现

bool isValid(string s) {
    stack<char> st;
    for (char c : s) {
        if (c == '(' || c == '[' || c == '{') {
            st.push(c);
        } else {
            if (st.empty()) return false;
            char top = st.top(); st.pop(); // read THEN pop
            if (c == ')' && top != '(') return false;
            if (c == ']' && top != '[') return false;
            if (c == '}' && top != '{') return false;
        }
    }
    return st.empty(); // unmatched openings → false
}

栈的其他经典用途

Min Stack(LC 155)
同时维护辅助栈存当前最小值,push 时同步,pop 时同步
用栈实现队列(LC 232)
两个栈模拟队列:in-stack 负责入队,out-stack 负责出队
每日温度(LC 739)
单调栈变体,见下节

常见错误

pop() 无返回值必须先 st.top() 读值,再 st.pop(),不能写 int x = st.pop()
空栈判断右括号时先判 st.empty(),否则 st.top() 未定义行为
仅检查括号种类([)] 虽然括号数量平衡但顺序非法,必须用栈逐一配对
练习:有效的括号代码练习
测试用例(5 个)
用例 1
输入:()
期望:true
用例 2
输入:()[]{}
期望:true
用例 3
输入:(]
期望:false
用例 4
输入:([)]
期望:false
用例 5
输入:{[]}
期望:true

单调递减栈:下一个更大元素

栈中元素从栈底到栈顶单调递减(按值)。当新元素比栈顶大时,栈顶元素找到了它的「下一个更大元素」,弹出并记录答案。

核心思想

栈存下标,栈中对应的值从底到顶单调递减。遍历数组时:若 nums[i] > nums[st.top()],则 st.top() 的「下一个更大元素」就是 nums[i],反复弹出直到栈空或栈顶 >= nums[i],然后把 i 入栈。最终还在栈里的元素答案为 -1。

步骤

  1. 初始化 ans 数组全为 -1,stack<int> st 存下标
  2. 从左到右遍历 i = 0..n-1
  3. while (!st.empty() && nums[st.top()] < nums[i]):ans[st.top()] = nums[i];st.pop()
  4. st.push(i)
  5. 遍历结束,栈中剩余下标对应答案保持 -1

C++ 实现

// Next Greater Element: for each nums[i], find the first element to its
// right that is strictly greater. Returns -1 if none exists.
vector<int> nextGreater(vector<int>& nums) {
    int n = nums.size();
    vector<int> ans(n, -1);
    stack<int> st; // stores indices; values decrease from bottom to top
    for (int i = 0; i < n; i++) {
        while (!st.empty() && nums[st.top()] < nums[i]) {
            ans[st.top()] = nums[i]; // st.top() found its answer
            st.pop();
        }
        st.push(i);
    }
    // remaining indices stay -1 (no greater element to the right)
    return ans;
}

变体

下一个更小元素
改为 nums[st.top()] > nums[i] 时弹出
环形数组(LC 503)
遍历两遍(i 从 0 到 2n-1,用 i%n 取元素)
每日温度(LC 739)
ans[i] = 下一个更大元素的下标差,而非元素值

常见错误

栈存值还是下标务必存下标!需要计算距离(每日温度)或有重复值时存值会出错
严格 vs 非严格下一个更大(严格 >)vs 下一个不小于(>=)对应不同弹出条件
遗漏初始化ans 应初始化为 -1(或 0 用于距离),不能用垃圾值
练习:下一个更大元素代码练习
测试用例(5 个)
用例 1
输入:4 4 5 2 10
期望:5 10 10 -1
用例 2
输入:5 2 1 2 4 3
期望:4 2 4 -1 -1
用例 3
输入:3 1 2 3
期望:2 3 -1
用例 4
输入:3 3 2 1
期望:-1 -1 -1
用例 5
输入:1 5
期望:-1

单调递增栈:柱状图最大矩形

栈中元素从栈底到栈顶单调递增(按高度)。当新柱比栈顶矮时,弹出栈顶并计算以该高度为上界的最大矩形面积。

核心思想

对于被弹出的柱子 heights[top]:它的左边界是新栈顶(因为栈中元素单调递增,栈顶左边的柱必然更矮),右边界是当前 i。面积 = heights[top] × (i - newTop - 1)。在数组末尾追加哨兵高度 0,确保所有元素最终都被弹出处理。

步骤

  1. 在 heights 末尾追加 0(哨兵),使最后所有元素都被弹出
  2. 遍历 i = 0..n(含哨兵)
  3. while (!st.empty() && heights[st.top()] > heights[i]):
  4. height = heights[st.top()]; st.pop()
  5. width = st.empty() ? i : i - st.top() - 1
  6. maxArea = max(maxArea, height * width)
  7. st.push(i)

C++ 实现

// Largest Rectangle in Histogram using monotonic increasing stack + sentinel
int largestRectangle(vector<int>& heights) {
    heights.push_back(0); // sentinel flushes all remaining bars
    int n = heights.size(), maxArea = 0;
    stack<int> st; // indices; corresponding heights increase bottom→top
    for (int i = 0; i < n; i++) {
        while (!st.empty() && heights[st.top()] > heights[i]) {
            int h = heights[st.top()]; st.pop();
            int w = st.empty() ? i : i - st.top() - 1;
            maxArea = max(maxArea, h * w);
        }
        st.push(i);
    }
    heights.pop_back(); // restore original array
    return maxArea;
}

常见错误

忘记哨兵不加末尾 0 时,递增序列的元素不会被弹出,面积漏算
宽度计算width = i - st.top() - 1(弹出后的新栈顶),若栈空则 width = i(从下标 0 开始)
重复高度相同高度的柱子会都留在栈里,哨兵会一次性弹出并正确计算
练习:柱状图中最大的矩形代码练习
测试用例(5 个)
用例 1
输入:6 2 1 5 6 2 3
期望:10
用例 2
输入:3 1 2 3
期望:4
用例 3
输入:1 5
期望:5
用例 4
输入:4 4 4 4 4
期望:16
用例 5
输入:3 2 0 2
期望:2

单调双端队列:滑动窗口最大值

用双端队列(deque)维护单调递减序列的下标:队首是当前窗口最大值的下标;队尾维护单调性,新元素进入时弹出队尾所有更小的元素。

核心思想

deque 中存下标,对应值从队首到队尾单调递减。新元素 nums[i] 入队前,从队尾弹出所有 < nums[i] 的元素(它们永远不会是窗口最大值);同时若队首下标已不在窗口内(< i-k+1),从队首弹出。窗口形成后(i >= k-1),队首即为当前窗口最大值下标。

步骤

  1. deque<int> dq 存下标,从队首到队尾对应值单调递减
  2. 遍历 i = 0..n-1
  3. 队首过期:while (!dq.empty() && dq.front() < i-k+1) dq.pop_front()
  4. 维护单调性:while (!dq.empty() && nums[dq.back()] < nums[i]) dq.pop_back()
  5. dq.push_back(i)
  6. 窗口就绪(i >= k-1):ans.push_back(nums[dq.front()])

C++ 实现

// Sliding Window Maximum using monotonic decreasing deque
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
    deque<int> dq; // stores indices; corresponding values decrease front→back
    vector<int> ans;
    for (int i = 0; i < (int)nums.size(); i++) {
        // 1. Remove expired front (outside window)
        while (!dq.empty() && dq.front() < i - k + 1)
            dq.pop_front();
        // 2. Maintain decreasing order from back
        while (!dq.empty() && nums[dq.back()] < nums[i])
            dq.pop_back();
        dq.push_back(i);
        // 3. Window is full: record maximum
        if (i >= k - 1) ans.push_back(nums[dq.front()]);
    }
    return ans;
}

变体

滑动窗口最小值
改为维护单调递增(队尾弹出 > nums[i] 的元素)
固定窗口中位数
需两个堆(见 heap/ 章节)
单调队列 + DP 优化
当 DP 转移 dp[i] = max(dp[j]) + cost 且 j 在固定区间内,可用单调队列优化到 O(n)

常见错误

先弹过期再维护单调过期检查(pop_front)和单调维护(pop_back)顺序不能搞反
存值而非下标存值无法判断队首是否过期,必须存下标
输出时机只有 i >= k-1 时才输出,共输出 n-k+1 个结果
练习:滑动窗口最大值代码练习
测试用例(5 个)
用例 1
输入:8 3 1 3 -1 -3 5 3 6 7
期望:3 3 5 5 6 7
用例 2
输入:4 2 4 3 2 1
期望:4 3 2
用例 3
输入:1 1 5
期望:5
用例 4
输入:5 1 1 2 3 4 5
期望:1 2 3 4 5
用例 5
输入:5 5 3 1 4 1 5
期望:5

LeetCode 题型

按结构分类的高频栈/队列题目。

栈基础

20. 有效的括号
左括号入栈,右括号弹栈匹配
155. 最小栈
辅助栈同步维护当前最小值
394. 字符串解码
栈存(重复数 + 已解码串),遇 ] 时弹出拼接
232. 用栈实现队列
两个栈模拟 FIFO

单调递减栈(求更大元素)

496. 下一个更大元素 I
单调栈模板题
739. 每日温度
答案存下标差
503. 下一个更大元素 II(循环数组)
遍历两遍取 i%n
901. 股票价格跨度
单调递减栈统计连续小于等于的天数

单调递增栈(求更小元素/面积)

84. 柱状图中最大的矩形
经典单调递增栈 + 哨兵
85. 最大矩形(0/1矩阵)
逐行转化为柱状图 + LC 84
402. 移掉 K 位数字
单调递增栈,弹出 k 个较大数字取最小结果
316. 去除重复字母
单调递增栈 + 剩余字符集判断

单调双端队列

239. 滑动窗口最大值
单调递减 deque 模板题
862. 和至少为 K 的最短子数组
前缀和 + 单调递增 deque
1438. 绝对差不超过限制的最长连续子数组
同时维护最大 deque 和最小 deque