单调栈:把「下一个更大的数」从 O(n²) 拉回 O(n)

每个元素最多进栈一次、出栈一次,所以看起来是双层循环的写法其实是线性。认出「左右第一个比自己大/小」这个形状,就能套同一个模板。

「对每个元素,找它右边第一个比它大的数」——暴力写法是两层循环。单调栈把它变成一次遍历,代价是承认栈里存的是还没找到答案的下标。

模板

从左往右扫,栈里保持一个递减序列;新元素来了,把比它小的都弹出去,那些被弹出去的元素的答案就是当前这个元素:

function nextGreater(nums) {
  const result = new Array(nums.length).fill(-1);
  const stack = [];
  for (let i = 0; i < nums.length; i += 1) {
    while (stack.length && nums[stack[stack.length - 1]] < nums[i]) {
      result[stack.pop()] = nums[i];
    }
    stack.push(i);
  }
  return result;
}

关键在于 while 里弹出去的元素当场就拿到了答案,以后不会再被讨论。所以内层 while 虽然看着像嵌套循环,总执行次数却是 O(n)。

为什么是 O(n)

摊还分析很简单:每个下标在 for 里入栈一次,最多被 pop 一次。总操作数 ≤ 2n,与数据分布无关。这也是它比「每个元素向后扫描」强的唯一原因 —— 后者在最坏情况下(递减序列)每次要扫到底。

同一形状的四个问题

问题 栈里维护 弹出时机
下一个更大元素 递减 新元素更大
柱状图最大矩形 递增 新元素更矮
接雨水 递减(存高度) 新元素更高
每日温度 递减(存下标) 新温度更高

认出「找左/右第一个比自己大/小」就能套模板;接雨水的变形是弹出时顺手算一块水的面积。

两个容易写错的细节

  • 栈里存下标而不是值:需要距离(每日温度问的是隔几天)或需要位置时,值不够用;存下标一定不会错。
  • 相等时弹不弹:< 还是 <= 决定重复元素得到的是「严格更大」还是「大于等于」。这个选择会影响结果里同一值的位置,写之前先想清楚要哪个语义。

只要题目在问「左边/右边第一个比我大/小的东西」,就先想栈,别先想双层循环。

← 返回文章列表

评论

…