单调栈:用一个栈,干掉所有「下一个更大」
栈里的元素始终保持有序,这个约束本身就是信息。
写在前面
「下一个更大的元素」「接雨水」「柱状图中的最大矩形」——这几道题第一次见很容易想到暴力,两层循环,O(n²)。
但如果你知道单调栈,这类题可以直接 O(n) 解决,而且代码并不复杂,难点只在于想清楚「栈里存什么」「什么时候弹出」。
单调栈是什么
单调栈就是普通的栈,但加了一个约束:栈内元素始终保持单调递增或单调递减的顺序 。
每次要压入新元素时,先把栈顶里不满足单调性的元素弹出,再压入。被弹出的那一刻,就是信息被「提取」的时刻。
两种形态
单调递增栈 (从栈底到栈顶递增):
1 2 3 4 栈底 → 栈顶:小 → 大 压入元素时,弹出所有 >= 新元素 的栈顶 → 被弹出时,新元素就是它「右边第一个更小的元素」
单调递减栈 (从栈底到栈顶递减):
1 2 3 4 栈底 → 栈顶:大 → 小 压入元素时,弹出所有 <= 新元素 的栈顶 → 被弹出时,新元素就是它「右边第一个更大的元素」
一句话记忆
元素被弹出的那一刻,触发弹出的新元素就是它苦苦等待的「答案」。
例题讲解
例题一:每日温度(LeetCode 739)
给定一个整数数组 temperatures,对于每天,找出下一个温度更高的那天还要等几天。如果之后没有更高温度的天,填 0。
输入:temperatures = [73, 74, 75, 71, 69, 72, 76, 73]
输出:[1, 1, 4, 2, 1, 1, 0, 0]
问题分析 :
对每个位置,找右边第一个比它大的元素的位置差。暴力 O(n²),单调栈 O(n)。
思路 :
用单调递减栈 (栈内温度从大到小):
栈里存下标 ,不存温度值(这样能算距离)
遇到比栈顶温度更高的新元素,把栈顶弹出,答案就是当前下标减栈顶下标
遍历完后,栈里剩下的元素说明后面没有更高温度,答案为 0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 vector<int > dailyTemperatures (vector<int >& temperatures) { int n = temperatures.size (); vector<int > ans (n, 0 ) ; stack<int > st; for (int i = 0 ; i < n; i++) { while (!st.empty () && temperatures[i] > temperatures[st.top ()]) { int j = st.top (); st.pop (); ans[j] = i - j; } st.push (i); } return ans; }
执行过程 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 temperatures = [73, 74, 75, 71, 69, 72, 76, 73] indices = [0, 1, 2, 3, 4, 5, 6, 7] i=0: 栈空,压入 0 栈: [0] ans: [0,0,0,0,0,0,0,0] i=1: 74>73,弹出 0,ans[0]=1-0=1 栈: [] 压入 1 栈: [1] ans: [1,0,0,0,0,0,0,0] i=2: 75>74,弹出 1,ans[1]=2-1=1 栈: [] 压入 2 栈: [2] ans: [1,1,0,0,0,0,0,0] i=3: 71<75,直接压入 栈: [2,3] i=4: 69<71,直接压入 栈: [2,3,4] i=5: 72>69,弹出 4,ans[4]=5-4=1 72>71,弹出 3,ans[3]=5-3=2 72<75,停止,压入 5 栈: [2,5] ans: [1,1,0,2,1,0,0,0] i=6: 76>72,弹出 5,ans[5]=6-5=1 76>75,弹出 2,ans[2]=6-2=4 栈空,压入 6 栈: [6] ans: [1,1,4,2,1,1,0,0] i=7: 73<76,直接压入 栈: [6,7] 遍历结束,栈中 [6,7] 未弹出,ans 保持 0 最终:[1, 1, 4, 2, 1, 1, 0, 0]
复杂度 :
时间:O(n),每个元素最多入栈一次、出栈一次
空间:O(n),栈的空间
例题二:下一个更大元素 II(LeetCode 503)
给定一个循环 数组,找每个元素的下一个更大元素。
输入:nums = [1, 2, 1]
输出:[2, -1, 2](第 3 个 1 的下一个更大是开头的 2)
问题分析 :
和 739 类似,但数组是循环的,最后几个元素要能「绕回来」看前面的元素。
思路 :
把数组遍历两遍 (下标对 n 取模模拟循环),但不需要真正复制数组:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 vector<int > nextGreaterElements (vector<int >& nums) { int n = nums.size (); vector<int > ans (n, -1 ) ; stack<int > st; for (int i = 0 ; i < 2 * n; i++) { int idx = i % n; while (!st.empty () && nums[idx] > nums[st.top ()]) { ans[st.top ()] = nums[idx]; st.pop (); } if (i < n) st.push (idx); } return ans; }
执行过程 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 nums = [1, 2, 1],n=3 第一遍(i=0~2): i=0: idx=0,栈空,压入 0 栈: [0] i=1: idx=1,2>1,弹出 0,ans[0]=2 栈: [] 压入 1 栈: [1] i=2: idx=2,1<2,压入 2 栈: [1,2] 第二遍(i=3~5,只弹不压): i=3: idx=0,nums[0]=1,1<2,没有弹出 i=4: idx=1,nums[1]=2,2>1(栈顶2),弹出 2,ans[2]=2 2>1(下一栈顶1的值是2,2==2?不大于),停止 此处 nums[1]=2,栈顶是 [1],nums[1]=2 不大于 nums[1]=2,停止 i=5: idx=2,nums[2]=1,1<2,没有弹出 最终:[2, -1, 2]
复杂度 :
例题三:柱状图中最大的矩形(LeetCode 84)
给定一个整数数组 heights 表示柱状图中各柱的高度,找出能勾勒出的最大矩形面积。
输入:heights = [2, 1, 5, 6, 2, 3]
输出:10
问题分析 :
对每根柱子,想知道以它的高度为矩形高度,最远能向左、向右延伸到哪里——也就是找左边第一个比它矮的柱子、右边第一个比它矮的柱子。
这正是单调递增栈 的用武之地:元素被弹出时,触发弹出的右边元素就是它右边第一个更矮的,栈中前一个元素就是它左边第一个更矮的。
思路 :
在数组两端各加一个高度为 0 的哨兵,避免特判边界:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int largestRectangleArea (vector<int >& heights) { heights.insert (heights.begin (), 0 ); heights.push_back (0 ); int n = heights.size (); int ans = 0 ; stack<int > st; for (int i = 0 ; i < n; i++) { while (!st.empty () && heights[i] < heights[st.top ()]) { int h = heights[st.top ()]; st.pop (); int left = st.top (); int width = i - left - 1 ; ans = max (ans, h * width); } st.push (i); } return ans; }
为什么加哨兵?
头部哨兵(高度 0):确保任何柱子都能在左边找到比它矮的边界,不用特判空栈
尾部哨兵(高度 0):确保遍历结束时栈里剩余的柱子都能被弹出处理
执行过程 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 加哨兵后 heights = [0, 2, 1, 5, 6, 2, 3, 0] indices = [0, 1, 2, 3, 4, 5, 6, 7] i=0: h=0,栈空,压入 0 栈: [0] i=1: h=2>0,压入 1 栈: [0,1] i=2: h=1<2,弹出 1 h=2,left=0,width=2-0-1=1,area=2*1=2,ans=2 h=1>=h[0]=0,压入 2 栈: [0,2] i=3: h=5>1,压入 3 栈: [0,2,3] i=4: h=6>5,压入 4 栈: [0,2,3,4] i=5: h=2<6,弹出 4 h=6,left=3,width=5-3-1=1,area=6*1=6,ans=6 h=2<5,弹出 3 h=5,left=2,width=5-2-1=2,area=5*2=10,ans=10 h=2>=h[2]=1,压入 5 栈: [0,2,5] i=6: h=3>2,压入 6 栈: [0,2,5,6] i=7: h=0<3,弹出 6 h=3,left=5,width=7-5-1=1,area=3*1=3 h=0<2,弹出 5 h=2,left=2,width=7-2-1=4,area=2*4=8 h=0<1,弹出 2 h=1,left=0,width=7-0-1=6,area=1*6=6 h=0>=h[0]=0,停止,压入 7 ans = 10
复杂度 :
例题四:接雨水(LeetCode 42)
给定 n 个非负整数,每个数表示柱子的高度,计算下雨后能接到多少雨水。
输入:height = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]
输出:6
问题分析 :
某个位置能接多少水,由它左边最高柱子和右边最高柱子中的较小者 减去自身高度决定。暴力做法 O(n²),有 O(n) 的双指针和单调栈两种解法,这里展示单调栈。
思路 :
用单调递减栈 (栈顶最小):当发现新柱子比栈顶高,说明出现了一个「凹槽」,可以接水:
弹出栈顶(凹槽底部)
新的栈顶是左边界,当前下标是右边界
宽度 = 右边界 - 左边界 - 1
高度 = min(左边界高度, 右边界高度) - 凹槽底部高度
面积 = 宽度 × 高度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int trap (vector<int >& height) { int ans = 0 ; stack<int > st; for (int i = 0 ; i < (int )height.size (); i++) { while (!st.empty () && height[i] > height[st.top ()]) { int bottom = st.top (); st.pop (); if (st.empty ()) break ; int left = st.top (); int width = i - left - 1 ; int h = min (height[left], height[i]) - height[bottom]; ans += width * h; } st.push (i); } return ans; }
执行过程 (简化版,取关键步骤):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 height = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1] idx = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11] i=0: h=0,压入 0 栈: [0] i=1: h=1>0,弹出 0(bottom) 栈空,无左边界,break 压入 1 栈: [1] i=2: h=0<1,压入 2 栈: [1,2] i=3: h=2>0,弹出 2(bottom=0) left=1,width=3-1-1=1 h=min(1,2)-0=1 ans += 1*1=1 ans=1 h=2>1,弹出 1(bottom=1) 栈空,break 压入 3 栈: [3] ...(继续类似步骤,i=4 压入 4,i=5 压入 5,i=6 会先弹出 5,再压入 6) 关键步骤 i=7(h=3): 此时栈为 [3,4,6],对应高度 [2,1,1] 弹出 6(bottom=1):left=4,right=7,width=7-4-1=2 h=min(height[4], height[7]) - height[6] = min(1,3)-1 = 0,ans 不变 弹出 4(bottom=1):left=3,right=7,width=7-3-1=3 h=min(height[3], height[7]) - height[4] = min(2,3)-1 = 1,ans += 3,变为 5 继续弹出 3 后栈空,停止 压入 7,栈: [7] 后续 i=10 时还能再接 1 格水,最终 ans = 6
复杂度 :
双指针解法(补充) :
接雨水还有一个空间 O(1) 的双指针解法,思路不同但更简洁:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int trap (vector<int >& height) { int left = 0 , right = height.size () - 1 ; int left_max = 0 , right_max = 0 ; int ans = 0 ; while (left < right) { left_max = max (left_max, height[left]); right_max = max (right_max, height[right]); if (left_max < right_max) { ans += left_max - height[left]; left++; } else { ans += right_max - height[right]; right--; } } return ans; }
两种方法都是 O(n),双指针空间更优,单调栈思路更统一,面试时能说出两种更好。
容易踩的坑
1. 栈里存下标,不存值
很多题需要计算宽度(位置差),所以栈里要存下标 ,通过下标找到对应的值。初学容易直接存值导致算不出距离。
1 2 3 4 5 stack<int > st; stack<int > st;
2. 弹出后别忘了判断栈是否为空
LeetCode 84 和 42 里,弹出元素后要找「前一个」作为左边界,但这时栈可能已经空了,要先判断。
1 2 3 int bottom = st.top (); st.pop ();if (st.empty ()) break ; int left = st.top ();
3. 递增栈 vs 递减栈
目标
栈的类型
被弹出的时机
找右边第一个更大 的
单调递减栈
压入更大元素时
找右边第一个更小 的
单调递增栈
压入更小元素时
最大矩形面积
单调递增栈
压入更小高度时
接雨水
单调递减栈
压入更大高度时
判断的时候想一句:「我什么时候能知道某个元素的答案?」—— 就是它被弹出的那一刻。
4. 取等号的问题
弹出条件用 > 还是 >= 会影响「相等元素要不要保留在栈里」:
>:遇到相等不弹,保留旧元素,适合把相等值也当作潜在边界的写法
>=:遇到相等也弹,适合希望边界保持严格更小 / 更大 、避免重复计算的写法
关键不是“结果不对就试一下”,而是先想清楚:相等元素在这道题里算不算有效边界 。
总结
mindmap
root((单调栈))
核心思想
栈内保持单调性
被弹出时提取答案
每个元素最多入栈出栈各一次 O(n)
两种栈
单调递减栈
找右边第一个更大
接雨水
单调递增栈
找右边第一个更小
最大矩形面积
经典题目
739 每日温度
503 下一个更大元素II 循环数组
84 柱状图最大矩形
42 接雨水
常见陷阱
栈存下标不存值
弹出后检查栈是否为空
递增递减选错
哨兵简化边界处理
各题对比 :
题目
栈类型
弹出条件
弹出时计算
739 每日温度
递减
新元素更大
距离差
503 下一个更大
递减
新元素更大
新元素值
84 最大矩形
递增
新元素更小
高 × 宽
42 接雨水
递减
新元素更大
凹槽面积
单调栈的核心就是一句话:让每个元素在被弹出的时候,告诉你它想告诉你的信息。 想清楚「什么时候弹」「弹出时能知道什么」,这类题基本上就拿下了。
推荐阅读 :