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

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

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

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

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

  • 单调递减栈:栈顶到栈底元素递减(如 [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,1] → [1,2,1,1,2,1]),用单调递减栈遍历 2 倍长度的数组,仅在第一圈(原数组长度内)入栈,第二圈只弹栈不入栈,避免重复处理。

详细图解

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

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

i nums[i] 栈内容 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 nums[i%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(无凹槽,左右边界高度相同但无中间更低的底部)。

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

相关推荐
ATaylorSu几秒前
经典算法之美:冒泡排序的优雅实现
开发语言·笔记·学习·算法
典学长编程5 分钟前
前端开发(HTML,CSS,VUE,JS)从入门到精通!第三天(JavaScript)
前端·javascript·css·html·前端开发
菜鸡nan1 小时前
23th Day| 39.组合总和,40.组合总和II,131.分割回文串
算法·leetcode·职场和发展
冷月葬花~1 小时前
day37 卡码网52. 携带研究材料 力扣518.零钱兑换II 力扣377. 组合总和 Ⅳ 卡码网57. 爬楼梯
算法
qq_513970441 小时前
力扣 hot100 Day63
数据结构·算法·leetcode
lifallen1 小时前
AbstractExecutorService:Java并发核心模板解析
java·开发语言·数据结构·算法
神器阿龙1 小时前
排序算法-归并排序
数据结构·算法·排序算法
遇见尚硅谷1 小时前
# C语言:20250730学习(二级指针)
c语言·学习·算法
Ashlee_code1 小时前
北极圈金融科技革命:奥斯陆证券交易所的绿色跃迁之路 ——从Visma千倍增长到碳信用衍生品,解码挪威资本市场的技术重构
科技·算法·金融·重构·架构·系统架构·区块链
我是why的狗2 小时前
赵义弘-----补题报告
算法·排序算法