栈,其实是一种“延迟决策”

0 阅读6分钟

核心心智:栈,其实是一种“延迟决策”

首先,我们要明白栈(Stack)的本质。栈,其实是一个高级的“待办事项清单”或“延迟决策(Deferred Decision-Making)”的结构。

当一个元素进入栈时,意味着:“我现在还不知道我最终的命运是什么,我需要先躺在栈里挂起(Pending),等待未来的元素来审判我。”

视角一:栈顶“主动”解套(拿走属于我的结果)

  • 画面感:栈顶元素在栈里相亲,手里拿着自己的“择偶标准”。新元素(新嘉宾)一登场,栈顶一看:“哇,正是我想要的,该我出山了”于是栈顶 开开心心地出栈。
  • 它解决的问题:面向未来(找右边)。因为未来还没发生,栈顶只能在栈里“被动等待”,直到右边出现满足它要求的元素。一旦出现,它就“主动”走人。
  • 经典对齐:
    • 找右边第一个比我小的数:栈顶是 4。新来了一个 84 发现 8 不满足要求,8入栈排队,等待后续更小的数字出栈,入栈了一个1,8发现1比自己小,满足了自己的要求,出栈,4发现1比自己小,满足自己的要求出栈。
    • 找右边第一个比我大的数:栈顶是 4(我的标准:我要比我大的!)。新来了一个 84 发现 8 满足要求,主动解套出栈,记录答案:4 的右边大数是 8。未来已来
    • 算术题里的乘号:栈顶是 *(我的标准:右边别来比我厉害的)。新来了一个 +* 一看,+ 没我厉害,说明我的右边界安全了,主动出栈去跟数字做结算。

视角二:栈顶“被动”被踢(江山代有才人出)

  • 画面感:栈顶是个老员工,占着茅坑。新员工(新元素)带着更强的简历来了,不仅能力比栈顶强,而且更年轻、更有未来(位置更靠右)。新员工冷酷地对栈顶说:“你被优化了,卷铺盖走人吧。”栈顶只能被动地被踢出栈。
  • 它解决的问题:回溯过去(找左边)。当新元素进来时,它要把左边所有“不如自己好”的老元素全部卷死踢走。等把不合格的人都踢光了,最后剩下的那个“老大哥”才是新元素要找的靠山。
  • 经典对齐:
    • 找左边第一个比我小的数:新来了一个 6。它往栈顶一看,是个 76 心想:“我比你小,你不是未来要找的人,至少胜过我才是未来要寻找的人”
    • 找左边第一个比我大的数:新来了一个 6。它往栈顶一看,是个 76 心想:“你比我大,有可能是未来要找的比较大的数,我先入栈等待”,此时来了一个8,8发现自己比6大,未来起码找的也是自己,6就啥也不是了,直接出栈,同样7也出栈

深度硬核:在“看左边”时,延迟决策是如何发生的?

在“看左边”(新元素踢走栈顶)的视角下,延迟决策依然存在,只是决策的主体发生了变化。

  • 看右边(栈顶主动)时,是栈内元素在延迟决策(我先在栈里等一等,等右边出现满足我要求的元素时,我再出栈结算答案)。
  • 看左边(新元素主动)时,是新元素在做延迟决策(由于我不知道未来会不会有更适合的元素,我先把我左边不合格的踢掉,然后把自己存进栈里,等待未来被决策)。

我们可以把“看左边”时的延迟决策拆解为两个核心阶段:

1. 过去无法挽回,但“现在”可以过滤不合格的过去

新元素到来时,它需要寻找它左边的某种边界(比如左边第一个比它小的数)。

  • 因为新元素是“现在”,它无法改变“过去”(栈里已经存在的元素)。
  • 它发现,栈顶那些比它大的元素,在未来绝对不可能作为任何后续元素的左边界了(因为当前新元素更小、更靠右,比栈顶更具优势)。
  • 于是,新元素果断把它们“踢走”。这个“踢走”的动作,就是在替未来做部分决策:帮未来的元素排除掉不可能的选项。

2. “把自己入栈”就是最纯粹的延迟决策

当新元素踢完了左边所有不合格的元素后,它会把自己压入栈中。这个入栈的动作,就是延迟决策的开始。

  • 为什么不立刻结算自己? 因为新元素(当前元素)并不知道自己在这个数组中的“最终历史使命”是什么。
  • 它可能是一个区间的左边界,也可能是一个更大区间的右边界。它需要等待未来的元素来决定它的命运。
  • 在它躺在栈里的这段时间,它的状态是“挂起(Pending)”的。直到未来某一个更大的元素或者更小的元素过来把它踢走,或者利用它作为左边界,它的价值才真正被结算。

经典案例:用《柱状图中最大的矩形》(LeetCode 84)来走一遍逻辑

假设我们看左边,要维护一个单调递增栈,当遇到新元素比栈顶小时,开始踢人。

  1. 入栈(延迟决策): 当我们遍历到高度为 5 和 6 的柱子时,我们不知道它们能向右延伸多远,所以把它们推进栈里。(让它们在栈里延迟等待)。
  2. 新元素触发踢人(看左边):当新元素 2 到来时,它往左看,发现栈顶是 6。“看左边”的逻辑触发了:62 高,6 不可能再往右延伸了。
  3. 结算过去(延迟的决策终于执行):
    • 新元素 26 踢出栈。
    • 此时,6 的右边界被确定了(就是新元素 2 的位置)。
    • 6 的左边界是谁?就是它在栈里的前一个元素 5 的位置!
    • 看!正是因为之前的“延迟决策”,把 56 存在了栈里,现在我们才能同时知道 6 的左边界(栈里的前一个)和右边界(当前新元素)。

核心口诀:四句话搞定所有单调栈

看完这个推演,你会发现单调栈的四种变阵(找左右、找大小),其实全都在下面这两行代码逻辑的组合里:

  1. 看左还是看右?

    • 找右边:新元素是“拯救者”,进栈的元素在里面被动等待被右边干掉。
    • 找左边:新元素是“捕猎者”,新元素来的时候主动清算栈里不合格的老元素。
  2. 栈里留大还是留小?

    • 只要找更小的数,就把大的踢掉(栈内保持单调递增)。
    • 只要找更大的数,就把小的踢掉(栈内保持单调递减)。