栈中藏玄机:从温度到雨水,单调栈的逆袭之路

一. 单调栈是什么?------ 生活中的 "排队" 哲学

还记得小时候排队买冰淇淋吗?假如每个人的身高都不一样,大家都想知道自己前面第一个比自己高的人是谁。你会怎么做?------ 没错,这就是单调栈的雏形!

单调栈,顾名思义,就是栈内元素保持单调(递增或递减)顺序。它的最大用处,就是帮我们高效地解决 "下一个更大 / 小元素" 这类问题。

关键修正:单调栈的 "方向性" 很重要!通常以 "栈顶到栈底" 为基准判断单调性:

  • 单调递减栈:栈顶到栈底元素递减(如 5,3,2),适合找 "下一个更大元素"(遇到比栈顶大的元素时弹栈)。
  • 单调递增栈:栈顶到栈底元素递增(如 2,3,5),适合找 "下一个更小元素"(遇到比栈顶小的元素时弹栈)。

生活小剧场:

小明排队时发现,只有比自己高的人出现,自己才会被 "弹出队伍",否则就一直等下去。

------"单调栈,排队不慌!"

二. 单调栈的基本套路与应用场景

单调栈的经典应用场景有:

  • 找下一个更大 / 更小元素
  • 区间最大 / 最小值
  • 面积 / 体积计算(如柱状图、接雨水)

套路总结

  1. 用栈存储元素的下标(而不是值),方便计算距离或区间长度。
  1. 遍历数组时,遇到 "破坏单调性" 的元素就弹栈并处理(如单调递减栈中遇到更大的元素)。
  1. 栈为空时要注意边界情况(避免无左边界时的错误计算)。

效率说明 :所有单调栈问题的时间复杂度都是 O(n) (每个元素仅入栈、出栈各一次,操作次数与元素数量线性相关)。

三. 题目实战

3.1 496. 下一个更大元素 I

题目简介

给定两个数组 nums1 和 nums2,nums1 是 nums2 的子集。对于 nums1 中的每个元素,找出它在 nums2 中右边第一个比它大的元素,没有则返回 -1。

解题思路

单调递减栈遍历 nums2,当遇到比栈顶大的元素时,栈顶元素的 "下一个更大元素" 就是当前元素,用哈希表记录映射关系;最后根据 nums1 查哈希表即可。

详细图解

假设 nums2 = 2, 1, 3, 5, 4,nums1 = 1, 2, 4

步骤 当前元素 栈内容(存下标) hashTable 映射 说明
1 2 0 {} 2 入栈(栈保持递减:2
2 1 0,1 {} 1≤2,入栈(栈保持递减:2,1
3 3 2 {1:3, 0:3} 3>1(破坏递减),弹 1,1 的下一个更大是 3;3>2(破坏递减),弹 0,2 的下一个更大是 3;3 入栈
4 5 3 {1:3, 0:3, 2:5} 5>3(破坏递减),弹 3,3 的下一个更大是 5;5 入栈
5 4 3,4 {1:3, 0:3, 2:5} 4≤5,入栈(栈保持递减:5,4

可视化动画

css 复制代码
初始:栈 []
遍历2:栈 [2](保持递减)
遍历1:栈 [2,1](保持递减)
遍历3:3>1 → 弹1(1→3);3>2 → 弹2(2→3);栈 [3]
遍历5:5>3 → 弹3(3→5);栈 [5]
遍历4:栈 [5,4](保持递减)

最终查 nums1

  • 1 → 3
  • 2 → 3
  • 4 → -1(栈中未弹出,无更大元素)

代码实现(带关键注释)

js 复制代码
var nextGreaterElement = function(nums1, nums2) {
    let hashTable = new Map(); // 存储nums2中元素的下一个更大值
    let stack = []; // 单调递减栈(存下标)
    for (let i = 0; i < nums2.length; i++) {
        // 当当前元素>栈顶元素时,栈顶元素的下一个更大值就是当前元素
        while (stack.length && nums2[i] > nums2[stack[stack.length - 1]]) {
            let index = stack.pop(); // 弹出栈顶(找到更大元素的元素)
            hashTable.set(nums2[index], nums2[i]); // 记录映射关系
        }
        stack.push(i); // 当前元素入栈,维持栈的递减性
    }
    // 遍历nums1,从哈希表中取结果(未找到则返回-1)
    return nums1.map(num => hashTable.get(num) ?? -1);
};

幽默点评

单调栈就像 "八卦小分队",谁比我高,谁是我的 "下一个目标",一查一个准!

3.2 503. 下一个更大元素 II

题目简介

给定一个循环数组(元素收尾相接),找每个元素的下一个更大元素。没有则返回 -1。

解题思路

循环数组可理解为 "数组拼接自身"(如 1,2,11,2,1,1,2,1),用单调递减栈遍历 2 倍长度的数组,仅在第一圈(原数组长度内)入栈,第二圈只弹栈不入栈,避免重复处理。

详细图解

nums = 1, 2, 1(循环后等效于 1,2,1,1,2,1

第一圈(i=0,1,2)

i numsi 栈内容 res 说明
0 1 0 -1,-1,-1 1 入栈(栈递减:1
1 2 1 2,-1,-1 2>1(破坏递减),弹 0,res 0=2;2 入栈(栈递减:2
2 1 1,2 2,-1,-1 1≤2,入栈(栈递减:2,1

第二圈(i=3,4,5,仅弹栈不入栈)

i numsi%3 栈内容 res 说明
3 1 1,2 2,-1,-1 1≤1(栈顶是 1),不弹栈
4 2 1 2,-1,2 2>1(栈顶是 1),弹 2,res 2=2;2≤2(栈顶是 2),停止弹栈
5 1 1 2,-1,2 1≤2,不弹栈

动画描述

  • 第一圈,2 把 1 弹出,1 的下一个更大是 2。
  • 第二圈,2 把最后一个 1 弹出(因为循环,这个 1 的右边有 2),1 的下一个更大是 2。

代码实现(带关键注释)

js 复制代码
var nextGreaterElements = function(nums) {
    let n = nums.length;
    let res = new Array(n).fill(-1); // 初始化结果为-1(默认无更大元素)
    let stack = []; // 单调递减栈(存下标)
    // 遍历2n长度(模拟循环数组)
    for (let i = 0; i < 2 * n; i++) {
        // 当前元素在原数组中的位置(循环处理)
        let current = nums[i % n];
        // 当当前元素>栈顶元素时,栈顶元素的下一个更大值是当前元素
        while (stack.length && current > nums[stack[stack.length - 1]]) {
            let index = stack.pop();
            res[index] = current; // 记录结果
        }
        // 只在第一圈入栈(避免重复入栈)
        if (i < n) stack.push(i);
    }
    return res;
};

陷阱提示

循环数组处理时,第二圈不能入栈!否则会导致栈中出现重复下标,计算错误。就像排队时不能重复排两次队,不然会被当成 "插队" 哦~

3.3 739. 每日温度

题目简介

给定一组每日气温,返回每一天需要等几天才会升温。如果之后都不会升温,返回 0。

解题思路

单调递减栈存储气温的下标,当遇到更高气温时,弹出栈顶下标,当前下标与栈顶下标的差值就是 "需要等待的天数"(因为这是第一个更高气温)。

详细图解

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

i 当前温度 栈内容(下标) res 说明
0 73 0 0,0,0,0,0,0,0,0 73 入栈(栈递减:73
1 74 1 1,0,0,0,0,0,0,0 74>73(破坏递减),弹 0,res 0=1-0=1;74 入栈
2 75 2 1,1,0,0,0,0,0,0 75>74(破坏递减),弹 1,res 1=2-1=1;75 入栈
3 71 2,3 1,1,0,0,0,0,0,0 71≤75,入栈(栈递减:75,71
4 69 2,3,4 1,1,0,0,0,0,0,0 69≤71,入栈(栈递减:75,71,69
5 72 2,3,5 1,1,0,2,1,0,0,0 72>69→弹 4,res 4=5-4=1;72>71→弹 3,res 3=5-3=2;72≤75,入栈
6 76 6 1,1,4,2,1,1,0,0 76>72→弹 5,res 5=6-5=1;76>75→弹 2,res 2=6-2=4;76 入栈
7 73 6,7 1,1,4,2,1,1,0,0 73≤76,入栈(栈递减:76,73

动画描述

  • 栈里存的都是 "还没等到升温" 的天数下标,像一群排队等通知的人。
  • 一旦出现更高温度("通知来了"),就从栈顶开始 "叫号",计算每个人等了几天。

代码实现(带关键注释)

js 复制代码
var dailyTemperatures = function(temperatures) {
    const len = temperatures.length;
    let res = new Array(len).fill(0); // 初始化结果为0(默认不升温)
    let stack = []; // 单调递减栈(存下标)
    for (let i = 0; i < len; i++) {
        // 当当前温度>栈顶温度时,栈顶这天的升温天数是i - 栈顶下标
        while (stack.length && temperatures[i] > temperatures[stack[stack.length - 1]]) {
            let index = stack.pop(); // 弹出栈顶(等待升温的天)
            res[index] = i - index; // 计算等待天数
        }
        stack.push(i); // 当前天入栈,等待后续更高温度
    }
    return res;
};

幽默点评

"升温预报员上线:有了单调栈,妈妈再也不用担心我穿错衣服了!今天穿毛衣,明天穿短袖?栈说了算~"

3.4 84. 柱状图中最大的矩形

题目简介

给定一组柱状图的高度,求能围成的最大矩形面积(矩形的边平行于坐标轴)。

解题思路

单调递增栈存储柱子下标(栈顶到栈底递增),当遇到更矮的柱子时,弹出栈顶柱子,以该柱子为高,计算最大矩形面积:

  • 左边界:新栈顶下标(左侧第一个更矮的柱子)
  • 右边界:当前下标(右侧第一个更矮的柱子)
  • 宽度 = 右边界 - 左边界 - 1
  • 面积 = 栈顶柱子高度 × 宽度

技巧:在数组首尾加 "哨兵"(高度 0),避免处理边界时栈空的情况。

详细图解

heights = 2,1,5,6,2,3 → 加哨兵后 0,2,1,5,6,2,3,0

i 当前高度 栈内容(下标) 计算面积 maxArea 说明
0 0 0 - 0 哨兵 0 入栈(栈递增:0
1 2 0,1 - 0 2>0,入栈(栈递增:0,2
2 1 0,2 2×1=2 2 1<2(破坏递增),弹 1,左边界 0,右边界 2,宽 = 2-0-1=1 → 面积 2×1=2;1 入栈
3 5 0,2,3 - 2 5>1,入栈(栈递增:0,1,5
4 6 0,2,3,4 - 2 6>5,入栈(栈递增:0,1,5,6
5 2 0,2,3,5 6×1=6, 5×2=10 10 2<6→弹 4,左边界 3,右边界 5,宽 = 5-3-1=1 → 6×1=6;2<5→弹 3,左边界 2,右边界 5,宽 = 5-2-1=2 → 5×2=10;2 入栈
6 3 0,2,3,5,6 - 10 3>2,入栈(栈递增:0,1,5,2,3
7 0 0,2,7 3×1=3, 2×4=8, 1×6=6 10 0<3→弹 6,左边界 5,右边界 7,宽 = 7-5-1=1 → 3×1=3;0<2→弹 5,左边界 3,右边界 7,宽 = 7-3-1=3 → 2×3=6?哦不对,原高度是 2(下标 5),宽 = 7-3-1=3 → 2×3=6?实际应为 2×(7-2-1)=2×4=8(左边界是 2,下标 2 对应高度 1);0<1→弹 2,左边界 0,右边界 7,宽 = 7-0-1=6 → 1×6=6;0 入栈

关键说明

当弹出下标 5(高度 2)时,新栈顶是下标 3(高度 5)吗?不,此时栈在弹 4、3 后,栈顶是下标 2(高度 1),所以左边界是 2,右边界是 7,宽度 = 7-2-1=4,面积 = 2×4=8。

代码实现(带关键注释)

js 复制代码
var largestRectangleArea = function(heights) {
    // 加首尾哨兵(高度0),避免边界处理
    heights.unshift(0);
    heights.push(0);
    let stack = [0]; // 单调递增栈(存下标),初始存入首哨兵
    let maxArea = 0;
    for (let i = 1; i < heights.length; i++) {
        // 当当前高度<栈顶高度时,栈顶柱子是"中间高",计算面积
        while (heights[i] < heights[stack[stack.length - 1]]) {
            let mid = stack.pop(); // 弹出栈顶(当前计算的柱子)
            let height = heights[mid]; // 矩形高度=栈顶柱子高度
            // 左边界是新栈顶(左侧第一个更矮的柱子),右边界是当前i(右侧第一个更矮的柱子)
            let width = i - stack[stack.length - 1] - 1;
            maxArea = Math.max(maxArea, height * width); // 更新最大面积
        }
        stack.push(i); // 当前柱子入栈,维持栈的递增性
    }
    return maxArea;
};

陷阱提示

忘记加哨兵会怎样?如果原数组是递增的(如 1,2,3),栈会一直入栈,遍历结束后栈内元素未弹出,导致漏算最大面积!哨兵就像 "保安",确保最后所有元素都能被 "叫号" 计算。

3.5 42. 接雨水

题目简介

给定一组柱子的高度,问下雨后能接多少水(柱子宽度为 1)。

解题思路

单调递减栈存储柱子下标,当遇到更高的柱子时,弹出栈顶柱子("凹槽底部"),此时:

  • 左边界:新栈顶下标(左侧较高的柱子)
  • 右边界:当前下标(右侧较高的柱子)
  • 凹槽高度 = min (左边界高度,右边界高度) - 凹槽底部高度
  • 凹槽宽度 = 右边界下标 - 左边界下标 - 1
  • 水量 = 凹槽高度 × 凹槽宽度

累加所有凹槽的水量即为总水量。

详细图解

height = 0,1,0,2,1,0,1,3,2,1,2,1

i 当前高度 栈内容(下标) 计算雨水 总水量 说明
0 0 0 - 0 0 入栈(栈递减:0
1 1 1 0 0 1>0→弹 0,栈空(无左边界),无法接水;1 入栈
2 0 1,2 - 0 0≤1,入栈(栈递减:1,0
3 2 1,3 1×1=1 1 2>0→弹 2(凹槽底部 0),左边界 1(高度 1),右边界 3(高度 2),h=1-0=1,w=3-1-1=1 → 水量 1;2>1→弹 1,栈空,2 入栈
4 1 1,3,4 - 1 1≤2,入栈(栈递减:2,1
5 0 1,3,4,5 - 1 0≤1,入栈(栈递减:2,1,0
6 1 1,3,4,6 1×1=1 2 1>0→弹 5(凹槽底部 0),左边界 4(高度 1),右边界 6(高度 1),h=1-0=1,w=6-4-1=1 → 水量 1;1≤1,入栈
7 3 1,3,7 2×3=6 8 3>1→弹 6(凹槽底部 1),左边界 4(高度 1),h=1-1=0→水量 0;3>1→弹 4(凹槽底部 1),左边界 3(高度 2),h=2-1=1,w=7-3-1=3 → 3×1=3;3>2→弹 3(凹槽底部 2),左边界 1(高度 0,实际是栈空?不,原栈顶是 1,高度 0),h=min (0,3)-2→无效,停止;3 入栈
... ... ... ... ... 后续累加剩余凹槽水量,最终总水量为 6

核心逻辑

每次弹出的 "凹槽底部" 必须被左右两个更高的柱子 "夹住" 才能接水,就像碗需要两边有边才能装水~

代码实现(带关键注释)

js 复制代码
var trap = function(height) {
    let n = height.length, res = 0, stack = []; // 单调递减栈(存下标)
    for (let i = 0; i < n; i++) {
        // 当当前高度>栈顶高度时,栈顶是"凹槽底部",计算水量
        while (stack.length && height[i] > height[stack[stack.length - 1]]) {
            let mid = stack.pop(); // 弹出凹槽底部
            if (!stack.length) break; // 栈空(无左边界),无法接水
            let left = stack[stack.length - 1]; // 左边界(栈顶,左侧较高的柱子)
            let right = i; // 右边界(当前柱子,右侧较高的柱子)
            // 凹槽高度=左右边界较矮的高度 - 凹槽底部高度
            let h = Math.min(height[left], height[right]) - height[mid];
            // 凹槽宽度=右边界-左边界-1(中间的格子数)
            let w = right - left - 1;
            res += h * w; // 累加水量
        }
        stack.push(i); // 当前柱子入栈,维持栈的递减性
    }
    return res;
};

幽默点评

"下雨天,单调栈帮你兜住每一滴水!不管是小水洼还是大水坑,栈一出手,全接住~"

四. 单调栈套路总结与常见陷阱

核心套路

  1. 栈存下标:方便计算距离 / 宽度(比存值更实用)。
  1. 单调性选择
    • 找 "更大元素" 用单调递减栈(遇大则弹)。
    • 找 "更小元素" 用单调递增栈(遇小则弹)。
  1. 处理逻辑:遍历数组时,用 "破坏单调性" 的元素触发弹栈,弹栈时计算目标值(值 / 距离 / 面积 / 水量)。

常见陷阱

  • 栈空判断:弹栈后若栈空,无左边界(如接雨水、柱状图),需停止计算。
  • 循环数组:仅在第一圈入栈,避免重复处理(如 503 题)。
  • 边界处理:柱状图、接雨水等问题可加 "哨兵"(高度 0),简化边界逻辑。
  • 计算目标混淆:区分是返回 "值"(如下一个更大元素)还是 "距离"(如每日温度)。

五. 单调栈学习路径与例题对比

学习路径(循序渐进)

  1. 基础:下一个更大元素 I(理解弹栈逻辑)→ 下一个更大元素 II(循环数组)。
  1. 进阶:每日温度(计算距离)→ 柱状图最大矩形(面积计算,单调递增栈)。
  1. 高阶:接雨水(凹槽水量,左右边界判断)。

例题核心对比表

题目 栈类型(顶→底) 核心计算目标 关键公式
下一个更大 I 单调递减 下一个更大元素的值 直接映射值
每日温度 单调递减 距离下一个高温的天数 i - 弹出下标
柱状图最大矩形 单调递增 最大矩形面积 高度 × (当前i - 新栈顶i - 1)
接雨水 单调递减 凹槽总水量 min(左高,右高)-中高 × (右i - 左i - 1)

六. 互动练习与思考题

练习题(附 LeetCode 思路)

  1. 下一个更小元素(LeetCode 496 变体):

输入 nums1=4,1,2, nums2=1,3,4,2,返回每个元素在 nums2 中右边第一个更小的元素(无则返回 - 1)。

提示:用单调递增栈,遇到比栈顶小的元素时弹栈记录映射。答案:2,-1,-1

  1. 柱状图优化思路

双指针法可解,但仅适用于 "无重复高度" 的场景;单调栈更通用,可处理所有情况。

思考题(带提示)

  1. 如果温度是递减的(如 5,4,3,2,1),单调栈会发生什么?

提示:栈会一直入栈(保持递减),最终所有结果都是 0(无更高温度)。

  1. 接雨水问题中,若所有柱子高度相同(如 3,3,3,3),能接多少水?

提示:0(无凹槽,左右边界高度相同但无中间更低的底部)。

掌握单调栈,就像掌握了 "排队的艺术"------ 谁在前、谁在后、谁比谁高,一目了然!刷题路上,栈随你动~ 🚀

相关推荐
_柳青杨2 小时前
一文吃透 Node.js 事件循环:从原理到 Node 20+ 重大变更
javascript·后端
JieE21212 小时前
LeetCode 101. 对称二叉树|JS 递归 + 迭代双解法,彻底搞懂镜像判断
javascript·算法
冬奇Lab14 小时前
AI Workflow 定义的四次演进:从 Markdown 到 JS 脚本,再到分布式多 Agent
javascript·人工智能·agent
一颗烂土豆20 小时前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
kyriewen1 天前
同事每天催我 Code Review,我写了个脚本让 AI 替我 review PR——现在他反过来催 AI 了
前端·javascript·ai编程
weedsfly1 天前
迭代器、生成器与异步迭代——让数据“按需流动”的艺术
前端·javascript
假如让我当三天老蒯1 天前
前端跨域解决方案(学习用)
前端·javascript·面试
铁皮饭盒1 天前
Bun 哪比 Node.js 快?
javascript·后端
JieE2121 天前
LeetCode 56. 合并区间|超清晰 JS 图解思路,面试高频区间题
javascript·算法·面试
candyTong2 天前
RTK 技术原理:一次典型会话里,80% 上下文是怎么省下来的
javascript·后端·架构