单调栈核心是用list模拟栈并维护严格递减序列,遍历时入栈前弹出所有破坏单调性的元素;常见于求下一个更大元素,时间复杂度O(n);循环数组通过索引取模与2n−1次遍历处理,栈中存原始索引且需防重复更新。

单调栈的核心逻辑:用栈维护递减序列
单调栈不是 Python 内置数据结构,而是用 list 模拟栈行为、并人为保证其内部元素单调(通常是严格递减)的一种技巧。关键不在于“建一个叫单调栈的类”,而在于“在遍历中,每次入栈前弹出所有破坏单调性的元素”。比如找下一个更大元素时,栈底到栈顶应保持递减——这样栈顶元素一旦遇到比它大的数,就能立刻确定它的答案,且这个答案就是当前遍历到的数。
常见错误是把栈当普通容器用,只 append 不 pop,结果栈越积越大,完全失去“单调”意义;或者弹出条件写反,比如写成 while stack and nums[i] ,那就变成维护递增栈了,和题目要求背道而驰。
标准实现:一次遍历 + 栈存索引
直接存数值看似简单,但无法回填答案位置;必须存索引,才能在弹出时更新 result[stack.pop()] = nums[i]。典型实现如下:
def nextGreaterElements(nums):
n = len(nums)
result = [-1] * n
stack = [] # 存索引
for i in range(n):
while stack and nums[i] > nums[stack[-1]]:
idx = stack.pop()
result[idx] = nums[i]
stack.append(i)
return result注意点:
立即学习“Python免费学习笔记(深入)”;
-
while循环必须用and连接两个条件,顺序不能颠倒:先判stack非空,再取stack[-1],否则空栈会报IndexError: list index out of range - 比较的是
nums[i] > nums[stack[-1]],不是nums[i] > stack[-1]—— 后者是在比索引值大小,毫无意义 - 每个索引最多入栈、出栈各一次,时间复杂度稳定
O(n),远优于暴力O(n²)
处理循环数组:遍历长度翻倍 + 取模
题目若要求「循环数组中下一个更大元素」(如 [1,2,1] 中最后一个 1 的答案是 2),不能真的复制数组(浪费空间),而是用索引取模模拟循环:
快速生成专业的 Python 脚本和应用代码。一键创建完整项目结构,支持CLI、API、爬虫、Bot、Django等多种项目类型,包含完整的项目结构、配置文件、依赖管理、测试、README和文档。
将循环展开为两倍长度遍历,但只允许每个原始索引被更新一次(靠 result 初始化为 -1 控制),代码只需微调:
def nextGreaterElementsCircular(nums):
n = len(nums)
result = [-1] * n
stack = []
# 遍历 2*n - 1 次,i 对应真实索引是 i % n
for i in range(2 * n - 1):
idx = i % n
while stack and nums[idx] > nums[stack[-1]]:
j = stack.pop()
if result[j] == -1: # 避免重复更新
result[j] = nums[idx]
stack.append(idx)
return result容易忽略的细节:
- 循环上限用
2 * n - 1而非2 * n:因为最坏情况下,最后一个元素要等绕一圈后第一个比它大的数,最多检查n-1个后续位置 - 必须加
if result[j] == -1判断:否则第二次遍历时可能覆盖已填的正确答案 - 栈里存的仍是原始索引(
idx),不是i,否则result[j]下标就错了
为什么不用 class 封装单调栈?
实际工程中极少定义 MonotonicStack 类。因为它的行为高度耦合于具体问题:弹出时机、比较逻辑、存值还是存索引、是否需要支持循环……全由上下文决定。强行封装反而增加理解成本和误用风险。比如你写了一个 push() 方法自动弹出小元素,但某天需求变成「找下一个更小元素」,你就得改方法名或加 flag 参数,不如每次按需手写几行 while 清晰。
真正该抽象的是「问题模式」:比如「左侧第一个更大元素」「右侧最近的最小值」——这些对应固定的遍历方向与栈单调性,但实现上依然推荐直接写逻辑,而不是套用某个“单调栈工具类”。
边界情况多、索引易错、循环处理要节制——这些才是写对的关键,不是语法糖能绕开的。

















