排序算法之:利用单调栈(Monotonic Stack)进行“下一个更大元素”(Next Greater Element)的求解与变种应用
题目描述
给你一个整数数组 nums,你需要为每个元素找出“下一个更大元素”。
“下一个更大元素”定义为:对于数组中的某个元素 nums[i],在它右侧找到第一个满足 nums[j] > nums[i] 的元素 nums[j]。如果不存在这样的元素,则结果为 -1。
例如:
输入:nums = [2, 1, 2, 4, 3]
输出:[4, 2, 4, -1, -1]
解释:
- 元素 2 右侧第一个比它大的是 4。
- 元素 1 右侧第一个比它大的是 2。
- 第二个 2 右侧第一个比它大的是 4。
- 元素 4 右侧没有比它大的元素,输出 -1。
- 元素 3 右侧没有比它大的元素,输出 -1。
解题过程循序渐进讲解
这个问题通常被看作一种“类排序”或“基于顺序的查找”问题,但它的高效解法并不依赖传统的比较排序,而是利用了一种叫做单调栈的数据结构。下面我们一步步来理解。
第一步:理解“下一个更大元素”的直观思路
最直接的想法是对每个元素,向右扫描直到找到比它大的元素。
对于数组长度为 n,最坏情况下需要对每个元素扫描整个右侧部分,时间复杂度是 O(n²)。
当 n 很大时(例如 10⁵),这种方法就太慢了。
我们需要找到一种方法,在扫描数组的过程中,能“记住”还未找到答案的元素,并在遇到更大的元素时,一次性解决多个元素的答案。
这正是单调栈的核心思想。
第二步:引入单调栈的概念
单调栈是一种特殊的栈,它保持栈内元素的某种单调性(单调递增或单调递减)。
在这个问题中,我们想要找到“下一个更大元素”,所以更适合使用单调递减栈(栈内元素从栈底到栈顶是递减的)。
为什么?
因为当我们从左到右遍历数组时:
- 如果当前元素 ≤ 栈顶元素,说明它不能作为栈内任何元素的“下一个更大元素”,我们把它压入栈,等待后面更大的元素。
- 如果当前元素 > 栈顶元素,那么它就是栈顶元素的“下一个更大元素”!我们可以弹出栈顶元素,并记录答案。而且,它可能还是栈中更多元素的答案(因为栈是递减的,栈内元素都比当前栈顶小,所以如果当前元素比栈顶大,它可能也比更早入栈的元素大)。
这个过程保证了每个元素只入栈、出栈一次,时间复杂度 O(n)。
第三步:用例子模拟单调栈的工作流程
以 nums = [2, 1, 2, 4, 3] 为例,初始化栈空,结果数组 res = [-1, -1, -1, -1, -1]。
-
i=0, nums[0]=2
栈空,入栈(存储索引 0)。栈:[0](对应值 2)。 -
i=1, nums[1]=1
栈顶元素值 2,1 ≤ 2,不满足“当前元素 > 栈顶”,所以入栈(索引 1)。栈:[0, 1](值 2, 1,递减)。 -
i=2, nums[2]=2
栈顶元素值 1,2 > 1,触发弹出:- 弹出栈顶索引 1,
res[1] = nums[2] = 2。
此时栈顶为索引 0,对应值 2,2 ≤ 2,不满足大于,所以停止弹出。
将当前索引 2 入栈。栈:[0, 2](值 2, 2)。
- 弹出栈顶索引 1,
-
i=3, nums[3]=4
栈顶元素值 2,4 > 2,触发弹出:- 弹出索引 2,
res[2] = 4。
栈顶变为索引 0,值 2,4 > 2,继续弹出: - 弹出索引 0,
res[0] = 4。
栈空,停止弹出。
入栈当前索引 3。栈:[3](值 4)。
- 弹出索引 2,
-
i=4, nums[4]=3
栈顶元素值 4,3 ≤ 4,不触发弹出,入栈索引 4。栈:[3, 4](值 4, 3)。
遍历结束,栈中剩余的元素(索引 3 和 4)右侧没有更大的元素,保持 res 中初始值 -1。
最终结果:res = [4, 2, 4, -1, -1]。
第四步:算法步骤总结
- 初始化一个空栈(存储元素索引),初始化结果数组
res全部为 -1。 - 从左到右遍历数组,对于每个索引
i和值nums[i]:- 当栈不为空,且
nums[i] > nums[栈顶索引]时:- 弹出栈顶索引
top_idx,并令res[top_idx] = nums[i]。
- 弹出栈顶索引
- 将当前索引
i压入栈中。
- 当栈不为空,且
- 遍历结束后,栈中剩下的索引在
res中仍是 -1(表示没有下一个更大元素)。
第五步:变种应用示例
这个单调栈的模板可以解决一系列“下一个更大/更小元素”的问题,只需调整单调性和比较方向:
- 下一个更大元素(循环数组):将数组拉直为两倍长度,用取模运算模拟循环,栈中存储索引仍用原数组长度。
- 下一个更小元素:改为维护单调递增栈(栈内递增),比较时判断
nums[i] < nums[栈顶]。 - 上一个更大元素:从右向左遍历,维护单调递减栈。
- 每日温度:求下一个更高温度的天数间隔,栈中存储索引,结果存
i - top_idx而不是值。
第六步:关键点与复杂度
- 核心思想:利用单调栈的单调性,保证每个元素出栈时,当前遍历到的元素就是它的“下一个更大元素”。
- 时间复杂度:O(n),每个元素最多入栈、出栈各一次。
- 空间复杂度:O(n),最坏情况下所有元素都压入栈。
这个技巧将看似需要 O(n²) 的搜索问题优化到了 O(n),是处理这类“顺序依赖查找”问题的强大工具。