单调栈基础与直方图与接雨水类

共 17 题
📑 题目列表 17 题
#
★★★

1. 为什么单调栈每个元素最多入栈出栈一次,从而整体 O(n)

为什么单调栈算法中每个元素最多被入栈和出栈各一次,从而使得整体时间复杂度为 O(n)?

  • 单调栈的线性复杂度证明
  • 状态空间与出入栈操作次数的关系
  • 摊还分析:虽然单元素可能停留很久,但 push/pop 总次数为 2n,均摊到每个元素为常数时间

单调栈的核心是维护一个元素在栈内保持单调有序的栈。在算法执行过程中,每个元素只会被压入栈一次(入栈时),也只会被弹出一次(当它破坏了单调性被后续元素弹出时)。弹出一个元素后它就不再回到栈中,因此全过程中每个元素至多经历一次 push 和一次 pop,总操作次数为 O(n),所以整体复杂度是 O(n)。这不依赖每个元素入栈后停留多久,只取决于"每个元素只进出一次"这一事实。

这是摊还分析(amortized analysis)的典型例子。虽然单个元素可能在栈中停留较久,但把全部 push/pop 操作数加起来得到的总量是 2n,均摊到每个元素上就是常数时间,因此整体线性。这也是单调栈与暴力嵌套循环(O(n²))的本质区别——暴力不断重复扫描,而单调栈每个元素只被看一次。

// Next Greater Element 的单调栈骨架
int[] nextGreater(int[] nums) {
    int n = nums.length;
    int[] res = new int[n];
    Deque<Integer> stack = new ArrayDeque<>(); // 存下标
    for (int i = n - 1; i >= 0; i--) {        // 从右往左
        while (!stack.isEmpty() && nums[stack.peek()] <= nums[i]) {
            stack.pop();                      // 每个元素最多被 pop 一次
        }
        res[i] = stack.isEmpty() ? -1 : nums[stack.peek()];
        stack.push(i);                        // 每个元素最多被 push 一次
    }
    return res;
}
#
★★★

2. 为什么接雨水的单调栈要维护递减(收集左侧更矮的墙)

为什么接雨水(LeetCode 42)的单调栈解法要维护一个递减的栈(栈底到栈顶递减),以便收集左侧更矮的墙?

  • 单调栈的方向与存储目标的关系
  • 接雨水"左右墙夹底"的几何结构
  • 手动推演:递增栈与递减栈各自的弹出条件与蓄水时机差异

接雨水的本质是找"被困在两堵更高墙之间的洼地"。当维护一个从栈底到栈顶递减的单调栈时,栈顶元素是当前遇到过的最矮的墙,它的左边是栈中比它高的墙。每当遇到一个比栈顶更高的墙时,栈顶(较矮的墙)就被夹在左边更高墙和右边当前更高墙之间,形成一个能蓄水的凹槽,此时即可计算水量。维护递减栈保证了"栈中元素从内到外(栈顶到栈底)高度递增",从而保证了每个被弹出的元素左侧一定有一堵更高的墙存在。

关键是要"从左往右"扫描时,栈保存的是尚未被右侧更高墙包住的候选墙。递减栈的栈顶永远是最矮的,遇到更高的墙时栈顶就被"夹住"形成可蓄水区域。若维护递增栈,则弹不出"被夹住"的较矮墙,无法形成蓄水逻辑。所以对"找左侧更高墙来夹住自己"的题目,必须维护递减栈。

int trap(int[] height) {
    int n = height.length, ans = 0;
    Deque<Integer> stack = new ArrayDeque<>(); // 递减栈,存下标
    for (int i = 0; i < n; i++) {
        while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
            int bottom = stack.pop();          // 被夹在中间的底
            if (stack.isEmpty()) break;        // 左边没有墙则无法蓄水
            int left = stack.peek();
            int w = i - left - 1;
            int h = Math.min(height[left], height[i]) - height[bottom];
            ans += w * h;
        }
        stack.push(i);
    }
    return ans;
}
#
★★★

3. 如何用单调递减栈求数组中每个元素的下一个更大元素(Next Greater)

如何用单调递减栈在线性时间内求出数组中每个元素的下一个更大元素(Next Greater Element)?

  • 单调栈两种方向(从左/从右)的实现
  • 递减栈的维护与弹出时机
  • 弹出时机:当前元素与栈顶比较时触发弹出的条件,以及"更大"与">=更大"的区分

两种常用方法:一是从右往左扫描,维护一个单调递减栈(从栈底到栈顶递减),对于每个元素,把栈中所有小于等于它的元素弹出,剩下的栈顶就是它右边第一个更大的元素,然后把它压栈;二是从左往右扫描,维护单调递减栈,当遇到一个比栈顶更大的元素时,该元素就是栈中所有被弹出元素的下一个更大元素。两种方法都充分利用了"每个元素只进出一次"的性质,复杂度 O(n)。注意相等元素要弹出(因为要找"更大"而非"大于等于")。

从右往左方法更直观:栈中保留的是"当前元素右侧所有尚未被更大元素淘汰的候选",单调递减保证栈内从底到顶越来越小,因此栈顶就是离当前元素最近且比它大的元素。从左往右方法则是在弹出时"结算"被弹出元素的下一个更大结果。两者的核心都是:一旦出现比栈顶更大的元素,栈中所有更小的元素的下一个更大元素就确定了。

int[] nextGreaterElement(int[] nums) {
    int n = nums.length;
    int[] res = new int[n];
    Deque<Integer> stack = new ArrayDeque<>();
    for (int i = n - 1; i >= 0; i--) {
        while (!stack.isEmpty() && nums[stack.peek()] <= nums[i]) {
            stack.pop();
        }
        res[i] = stack.isEmpty() ? -1 : nums[stack.peek()];
        stack.push(i);
    }
    return res;
}
#
★★★

4. 循环数组(LeetCode 503)如何用"遍历两遍+取模"处理环绕

在循环数组(LeetCode 503、Next Greater Element II)中,如何用"遍历两遍+下标取模"的方式处理数组的环绕问题?

  • 循环数组的展开技巧
  • 单调栈与取模下标的结合
  • 取模下标的环绕处理:i % n 将环形数组展开为线性,避免越界与重复计算

循环数组可以等价看成一个长度为 2n 的数组的前 n 个元素,即把原数组复制拼接一遍。这样每个元素的下一个更大元素(若存在)一定出现在前 2n 个位置内,不会超过一圈。实现时下标 i 从 0 扫描到 2n-1,用 i % n 获取真实下标。由于要保证结果的新鲜度,得到的答案只覆盖前 n 个位置,超出 n 的元素因为是"复制品"不需要写答案。这样既处理了环绕,又保持了单调栈的 O(n) 复杂度。

关键点在于"遍历两遍"的上界是 2n 而非 n:因为真正的下一个更大元素可能绕到数组开头,即第二个副本里。同时扫描到第二遍时,栈中已经积累了第一遍的结果,所以第二遍的弹出操作能正确结算第一遍残留的候选。取模 i % n 只是为了让下标能正确索引到原数组并写入结果。

int[] nextGreaterElements(int[] nums) {
    int n = nums.length;
    int[] res = new int[n];
    Arrays.fill(res, -1);
    Deque<Integer> stack = new ArrayDeque<>();
    for (int i = 0; i < 2 * n; i++) {
        int idx = i % n;
        while (!stack.isEmpty() && nums[stack.peek()] < nums[idx]) {
            res[stack.pop()] = nums[idx];
        }
        stack.push(idx);
    }
    return res;
}
#
★★★

5. 接雨水双指针 O(1) 空间法相比单调栈的取舍

接雨水的双指针 O(1) 空间解法相比单调栈解法,在时间、空间和实现难度上有哪些取舍?

  • 双指针法的空间优化
  • 时间与空间复杂度权衡
  • 短板决定水量:左右迭代指针中较矮一侧决定当前可蓄水量,另一侧保持不动

双指针法用左右两个指针从两端向中间收缩,维护 leftMax 和 rightMax 两个"当前已见到的最高墙",每次移动较矮指针那一侧,直接累加水量,空间复杂度 O(1),时间复杂度 O(n)。单调栈法同样 O(n) 时间,但需要 O(n) 的栈空间。双指针的实现更简洁、空间更优,但思路不如单调栈直观,需要理解"每侧只要知道当前最高墙即可蓄水"的原理。单调栈则更通用,能处理"按位置结算"的更多场景。

双指针的关键洞察是:对于某个位置,它上面能存的水取决于 min(左侧最高, 右侧最高) - 自身高度。当左右两指针未相遇时,若左指针位置较低,则其左侧最高已确定(leftMax),而右侧最高至少是 rightMax,但水量只由"较矮一侧"决定,所以低的一侧可以安全结算。这是"短板决定水量"的贪心思想,与"盛水容器"(11)同源。

int trap(int[] height) {
    int n = height.length, left = 0, right = n - 1;
    int leftMax = 0, rightMax = 0, ans = 0;
    while (left < right) {
        if (height[left] < height[right]) {
            leftMax = Math.max(leftMax, height[left]);
            ans += leftMax - height[left];
            left++;
        } else {
            rightMax = Math.max(rightMax, height[right]);
            ans += rightMax - height[right];
            right--;
        }
    }
    return ans;
}
#
★★★

6. 接雨水(42)单调栈解法的"左右墙夹底"水量累加逻辑

接雨水(LeetCode 42)单调栈解法中,"左右墙夹住底部"的水量累加逻辑是怎样的?

  • 单调栈接雨水的逐层累加
  • 宽度与高度的计算
  • 几何结构:栈底到栈顶高度递增,形成"左高右高夹底"的蓄水槽

单调栈用水量是"分块逐层"累加的。当遇到一个比栈顶更高的墙 i 时,先弹出栈顶作为"底部" bottom(被夹住的洼地最低点),此时栈中新的栈顶是左侧墙 left,当前 i 是右侧墙。这一层的水量 = 宽度 (i - left - 1) × 高度 (min(height[left], height[i]) - height[bottom])。因为 bottom 是当前被夹的最矮墙,其上方的水被左右墙限定,所以一次性累加这一层,然后继续弹出下一个更矮的底部,逐层累加直到栈顶不再矮于当前墙。

关键是要理解"逐层结算"而非"按列结算"。每次弹出 bottom 时,它和左右墙构成的这一层水才被确定,因为这一层的高度受限于左右墙的较低者。而 bottom 上面可能还有更高层,由后续弹出的元素继续结算。这样单次弹出就能准确累加一层水,符合"每个元素只进出一次"的线性性质。

int trap(int[] height) {
    Deque<Integer> stack = new ArrayDeque<>();
    int ans = 0;
    for (int i = 0; i < height.length; i++) {
        while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
            int bottom = stack.pop();
            if (stack.isEmpty()) break;
            int left = stack.peek();
            ans += (i - left - 1) * (Math.min(height[left], height[i]) - height[bottom]);
        }
        stack.push(i);
    }
    return ans;
}
#
★★★

7. 柱状图最大矩形(84)如何用单调递增栈找每根柱的左右边界

柱状图中最大矩形(LeetCode 84)如何用单调递增栈找到每根柱子的左右边界?

  • 递增栈与"左右第一个更矮"的关系
  • 以每根柱为高的矩形扩展
  • 弹出时结算:以被弹出柱为高、左右更矮柱为边界,计算矩形面积

维护一个从栈底到栈顶递增的单调栈。遍历时,当遇到一个比栈顶更矮的柱子 h[i] 时,栈顶柱子 h[t] 就再也无法向右扩展了,此时 h[t] 的右边界就是 i(右边第一个更矮的柱子),左边界就是它弹出后栈中新的栈顶 h[stack.peek()](左边第一个更矮的柱子)。于是以 h[t] 为高的矩形宽度 = i - stack.peek() - 1,面积 = h[t] × 宽度。遍历结束后栈中剩余元素按同样规则处理,右边界为 n。这样每个柱子的最大矩形都能 O(1) 结算。

递增栈保证"栈中元素从底到顶越来越高",因此弹出栈顶 t 时,新栈顶 peek 是 t 左边第一个比它矮的柱子,而当前 i 是 t 右边第一个比它矮的柱子,两者之间就是 t 能覆盖的最大宽度。这是"找下一个更小"(右界)+ "找前一个更小"(左界)的组合,正是单调栈的经典应用。

int largestRectangleArea(int[] heights) {
    int n = heights.length;
    int[] h = new int[n + 1]; // 末尾加 0 哨兵,强制清空栈
    System.arraycopy(heights, 0, h, 0, n);
    Deque<Integer> stack = new ArrayDeque<>();
    int ans = 0;
    for (int i = 0; i <= n; i++) {
        while (!stack.isEmpty() && h[stack.peek()] > h[i]) {
            int t = stack.pop();
            int left = stack.isEmpty() ? -1 : stack.peek();
            ans = Math.max(ans, h[t] * (i - left - 1));
        }
        stack.push(i);
    }
    return ans;
}
#
★★★

8. 每日温度(739)与在线股票跨度(901)的本质是否都是 Next Greater

每日温度(LeetCode 739)与在线股票跨度(LeetCode 901)的本质是否都是 Next Greater(下一个更大元素)问题?

  • 问题归约与模式识别
  • Next Greater 的变体
  • 在线与离线:股票跨度需在线处理,单调栈可一次扫描同时覆盖两种场景

是的,两者本质都是 Next Greater,只是数据形态不同。每日温度 739 求的是"每个位置右边第一个更大元素的下标距离",即标准 Next Greater 的索引差,用单调递减栈从右往左或从左往右均可。在线股票跨度 901 求的是"当前价格左边连续小于等于它的天数",等价于"前一个更大元素"确定跨度边界,即左边界 = 左边第一个更大元素的位置,跨度 = 当前下标 - 左边界下标。两者都只是 Next Greater 系列的方向/边界变体。

739 是"右边的下一个更大",901 是"左边的上一个更大(跨度)"。901 由于是流式在线,用单调递减栈(栈内价格递减)从左往右维护,每次新价格把栈顶小于等于它的弹出,剩下的栈顶就是左边第一个更大的,跨度 = i - 栈顶层数。因此能用"Next Greater 模板"统一理解,只是一个算右边、一个算左边。

// 739 每日温度
int[] dailyTemperatures(int[] t) {
    int n = t.length;
    int[] res = new int[n];
    Deque<Integer> stack = new ArrayDeque<>();
    for (int i = 0; i < n; i++) {
        while (!stack.isEmpty() && t[stack.peek()] < t[i]) {
            int j = stack.pop();
            res[j] = i - j; // 存右边界距离(下标差)
        }
        stack.push(i);
    }
    return res;
}
#
★★★

9. 接雨水的双指针解法中左右指针中较矮一侧决定当前水量,移动规则是什么?

接雨水的双指针解法中,为什么左右指针中较矮的一侧决定当前水量,移动规则是什么?

  • 短板决定水量的原理
  • 双指针移动规则
  • 移动不变量:每次移动较矮一侧指针,保证已走过的区域不会影响后续水量

每个位置能存的水由 min(左侧最高, 右侧最高) - 自身高度 决定。当左右指针未相遇时,若 height[left] < height[right],那么左侧最高 leftMax 已经确定,而右侧最高至少不会低于 rightMax,所以"左侧最高"是当前这个位置的决定因素(min 一定落在左侧),因此 left 位置的存水量 = leftMax - height[left] 可以安全结算,然后 left 右移。反之若右侧矮,则移动右指针。移动规则总结为:谁矮就结算谁、移动谁。

关键洞察是"min 的较矮一侧已被完全确定"。因为较矮指针那一侧的"最高"已经确定(就是它扫过的 max),而另一侧即使更高也不影响 min 的取值。所以不需要等另一侧指针到达,就能安全结算当前较矮指针位置。这保证了每个位置只结算一次,空间 O(1)。

int trap(int[] height) {
    int l = 0, r = height.length - 1, lm = 0, rm = 0, ans = 0;
    while (l < r) {
        if (height[l] < height[r]) {
            lm = Math.max(lm, height[l]);
            ans += lm - height[l];
            l++;
        } else {
            rm = Math.max(rm, height[r]);
            ans += rm - height[r];
            r--;
        }
    }
    return ans;
}
#
★★

10. 单调栈存储索引而非值的必要性(便于算距离/区间长度)

为什么单调栈通常存储索引(下标)而不是元素值,这样有什么好处?

  • 索引与值的取舍
  • 距离/区间长度计算
  • 索引即高度定位:通过下标得到值与位置,便于计算跨度与距离

存储索引而非值主要有两个好处:一是能通过下标差计算距离、区间长度或跨度(如每日温度的下标距离、最大矩形的宽度)。二是能通过下标访问原数组重新取值,一样能拿到值,而只存值则丢失了位置信息。存储索引后,在需要比较时用 nums[stack.peek()] 取值比较,在结算时用下标差算宽度/距离,灵活且信息完整。

单调栈的很多应用(最大矩形、接雨水、每日温度、股票跨度)都需要"位置"信息来计算宽度或距离。只存值只能得到"哪个更大",得不到"相隔多远"。所以约定俗成是存下标,需要大小时用数组回查。这也是单调栈能同时支持"比较大小"和"算距离"两类需求的关键。

#
★★

11. 如何判断一道题"该用单调栈",题目中出现"最近/边界/跨度"的线索

如何判断一道题应该使用单调栈?题目中出现哪些"线索"(如最近、边界、跨度)时该考虑单调栈?

  • 问题识别与模式匹配
  • 单调栈的适用场景
  • 特征归纳:从题目中的"最近""大于/小于""跨度"等关键词识别单调栈适用性

当题目要求求"每个元素右/左边第一个比它大/小的元素"、"最近的一个更大/更小"、"以某元素为边界/跨度/范围的题目"、"子数组的某种最值统计"等,都强烈提示使用单调栈。典型线索词包括:最近(next greater/smaller)、边界(左右边界)、跨度/连续段、直方图、接雨水、最大矩形、可见人数等。这些题的核心是"找比当前更大/更小的左右邻",而单调栈恰好在线性时间内给出每个元素的这些信息。

遇到"每个元素 + 左右第一个更大/更小"就能想到单调栈。当题目要求"每个位置最近的大于/小于它的元素"、"以某个元素为最小值的最大区间"、"能覆盖/可见的连续段"时,本质都在求"前一个/下一个更小/更大",正是单调栈的用武之地。这是识别题型的快捷方式。

#
★★

12. Maximal Rectangle(85)如何把矩阵逐行转化为直方图复用 84

Maximal Rectangle(LeetCode 85)如何把矩阵逐行转化为直方图,从而复用柱状图最大矩形(84)的解法?

  • 逐行累积高度
  • 问题归约到直方图
  • 逐行累积高度:以矩阵每行为基线,把列连续 1 的个数累加成直方图

对每一行 i,计算以该行为底、向上连续的 1 的个数作为"高度"数组 heights[j]:若 matrix[i][j]=='1' 则 heights[j]++,否则重置为 0。这样每一行就对应一个直方图,矩形的最大面积可通过在每一行上运行 84 的单调栈解法求出,取所有行的最大值即为答案。整体复杂度 O(rows × cols)。

关键是把"全 1 子矩形"问题转化为"直方图最大矩形"问题:一行一行更新高度数组,每行都求一次当前直方图的最大矩形,所有行取最大。因为任意全 1 矩形都可以被其底部所在行的直方图覆盖,所以逐行转化不遗漏任何解。这是二维问题归约为一维的经典技巧。

#
★★

13. 含障碍/unbounded 的接雨水变种如何调整边界处理

含障碍、边界不封闭(unbounded)等接雨水变种,如何调整边界处理?

  • 边界条件的泛化
  • 单调栈/双指针的边界适配
  • 障碍与越界:当遇到障碍或越过边界时,如何终止当前蓄水计算

标准接雨水假设左右边界外没有墙,水不会从两端溢出。若引入障碍(某些位置不能蓄水)或边界不封闭(某侧没有墙),则需要在结算时判断"是否真的存在足够的左右墙"。例如单调栈法,当弹出底部后栈为空(说明左边没有更高的墙)时,该次无法蓄水,直接跳过;双指针法则需要把障碍高度视为无穷大或跳过。针对变种,核心是"只有两侧都有墙才蓄水",且蓄水高度受限于 min(两侧墙) 与被障碍隔断的情况。

变种题的关键是明确"边界条件":哪些位置算墙、两侧是否封闭、障碍是否阻断水的连通。单调栈法天然适合"左侧必须存在墙"的判定(栈空即跳过);双指针法适合调整左右边界。理解基本蓄水逻辑后,边界调整只是加条件判断。

#
★★

14. 如何把"最大矩形/接雨水"抽象成"找比当前更小/更大的左右邻"

如何把"最大矩形/接雨水"这类问题抽象成"找比当前更小/更大的左右邻"?

  • 问题抽象与归约
  • 单调栈的统一定位
  • 统一定义:把"左右第一个更小/更大"抽象为单调栈的通用目标

最大矩形(84)的本质是:对每根柱子,找它左右第一个更矮的柱子作为边界,从而确定以它为高的最大宽度。接雨水(42)的本质是:对每个位置,找左右两侧更高的墙来确定水位。两者都归结为"求每个元素的前一个/下一个更小或更大元素"这一对单调栈能 O(n) 给出的信息。因此,只要把题目抽象成"以某元素为界限计算扩展区间",就能复用单调栈。

这类题的共同结构是"对每个元素求一个以它为中心/边界的区间",区间端点由"第一个更小/更大"决定。单调栈恰好能同时给出每个元素的前一个更小和下一个更小(或更大),于是区间长度 O(1) 可得。先抽象出"找左右邻"的目标,再套单调栈,是解题捷径。

#

15. 单调递增栈求前一个更小元素(Previous Smaller)的实现要点

用单调递增栈求前一个更小元素(Previous Smaller Element)的实现要点是什么?

  • 递增栈的语义
  • 前一个更小元素的求解
  • 递增栈语义:栈内从底到顶递增,保证栈顶是当前元素左侧最近更小者

求前一个更小元素,从左往右扫描并维护一个单调递增栈(从栈底到栈顶递增)。对于每个元素 nums[i],把栈中所有大于等于 nums[i] 的元素弹出,因为它们不可能是后续元素的前一个更小(且会被更小的当前元素挡住)。弹出后栈顶(若存在)就是 nums[i] 的前一个更小元素,然后把 nums[i] 压栈。这是"找左边第一个更小"的标准套路。

维护递增栈保证栈内元素严格递增,栈中元素是"未被淘汰的候选更小值"。新元素把大于等于它的都弹出,因为那些元素位置靠前又比当前大,对后续元素而言当前元素更小且更近,所以它们永远不会成为"前一个更小"。栈顶就是当前元素左边第一个更小。

#

16. 最大矩形中如何用一个单调递增栈同时弹出求 left/right 边界

在柱状图最大矩形中,如何用一个单调递增栈在一次弹出中同时求出 left 和 right 边界?

  • 弹出时的左右边界
  • 递增栈的一次性结算
  • 一次性结算:弹出时同时确定左右边界,避免二次扫描

当扫描到柱子 i 且 h[i] < h[stack.peek()] 时,弹出栈顶 t。此时 t 的右边界就是当前 i(右边第一个比它矮的柱子),左边界就是 t 弹出后新的栈顶位置 peek(若栈空则为 -1,表示左边界是最左端)。区间 [peek+1, i-1] 就是 t 能覆盖的最大宽度,面积 = h[t] × (i - peek - 1)。这样一次弹出同时得到左右边界,无需额外存储。

递增栈的性质保证了弹出顺序的有序性:栈中元素自底向上递增,弹出 t 后,新栈顶 peek 是 t 下面紧邻的、比 t 矮的柱子,正好是 t 的左边界;而当前 i 是触发弹出的、比 t 矮的柱子,正好是 t 的右边界。一次弹出同时拿到左右界,正是单调栈高效的原因。

#

17. 如何把"下一个更大/更小"统一成模板(哨兵+方向)避免记忆多套

如何把"下一个更大/更小"统一成一个模板(使用哨兵和方向),避免记忆多套代码?

  • 模板化抽象
  • 方向与比较符号的统一
  • 哨兵与方向:用哨兵简化边界,用方向统一"下一个/前一个"的遍历逻辑

可以统一为:遍历方向(从左/从右)选择"下一个"或"前一个",比较符号(< 或 >)选择"更大"或"更小",配合末尾哨兵(如压入 0 或极值)强制清空栈。例如"下一个更大"用从右往左扫描 + 递减栈 + 弹出 ≤ 栈顶;"前一个更小"用从左往右 + 递增栈 + 弹出 ≥ 栈顶。只要记住"方向定下一个/前一个,比较符号定更大/更小,哨兵兜底清栈",就能套用同一套思想。

四种组合(下一个更大、下一个更小、前一个更大、前一个更小)只是"扫描方向"和"比较符号"两个维度排列组合。方向决定"下一个(右)还是前一个(左)",符号决定"更大(<)还是更小(>)"。哨兵(末尾加一个极值)保证最后栈内元素也被清空结算。记住这两个维度就能推导出所有变体,不必死记四套代码。