Leetcode 901:单调栈

文章内容

LeetCode 901. 股票价格跨度

一、题目解析

1.1 题目描述

设计一个 StockSpanner 类,每次调用 next(price) 时,返回当天股票价格的跨度。
"跨度"的定义:从今天往回数,连续有多少天的股价 ≤ 今天的价格(含今天)。
换句话说,对于每一天,找到左边第一个比它大的那天,两者之间的天数就是跨度。

1.2 示例走一遍

plain text
输入: [100, 80, 60, 70, 60, 75, 85]输出: [  1,  1,  1,  2,  1,  4,  6]
天数 价格 为什么是这个跨度
1 100 前面没数据,跨度=1
2 80 左边 100 > 80,停下,跨度=1
3 60 左边 80 > 60,停下,跨度=1
4 70 60 ≤ 70 ✓, 80 > 70 ✗,跨度=2
5 60 左边 70 > 60,停下,跨度=1
6 75 60,70,60 都 ≤ 75, 80 > 75 ✗,跨度=4
7 85 75,60,70,60,80 都 ≤ 85, 100 > 85 ✗,跨度=6

关键观察:第 6 天价格 75,往左数到 80(第一个 > 75)之间,有 60、70、60 三个价格都 ≤ 75。这三个价格正是第 3、4、5 天。如果我们在第 4 天已经知道它"吞掉"了第 3 天的 60,那第 6 天就不需要逐一检查第 3 天——直接拿第 4 天打包好的 span 就行。

二、思路分析

2.1 暴力:回退扫描

最直接的想法:每次 next(price) 从当天往回扫,直到碰到比它大的价格或数组起点。

java
int span = 1;for (int i = prices.size() - 2; i >= 0 && prices.get(i) <= price; i--) {    span++;}

每次调用 O(n),n 次调用就是 O(n²)。在数据量较大时不可行。

2.2 单调栈:成段吞并

观察暴力过程的浪费:如果连续好几天价格都很低,每次都要一个个加过去。能不能把"一段连续的低价"打包成一个整体?

递减单调栈正好做这件事。

栈里存的不是单个价格,而是 {price, span} 对——price 是当时的价格,span 是"以这个价格为右端点、向左的连续有效天数"。栈内价格从底到顶严格递减。

每次来一个新价格 price:

  1. span = 1(自己至少算一天)
  2. 看栈顶:如果栈顶的 price ≤ 当前 price,说明栈顶那一段"被今天盖过去了",弹出栈顶、把它的 span 累加到 span 中,然后继续看新的栈顶
  3. 直到栈空或栈顶 price > 当前 price,把 {price, span} 压入栈
  4. 返回 span

这个过程叫"吞并":不是把栈里每一个元素 pop 出来加 1,而是把整段的 span 一次性"吃"掉。

三、代码实现

java
class StockSpanner {     // 单调递减栈:每个元素是 {price, span}    // span 表示以该 price 为右端点时,能向左覆盖的连续天数    private final Deque<int[]> stack = new ArrayDeque<>();     public StockSpanner() {    }     public int next(int price) {        int span = 1; // 今天自己算一天         // 栈顶价格 ≤ 当前价格 → 整段吞并        while (!stack.isEmpty() && stack.peek()[0] <= price) {            span += stack.pop()[1]; // 累加被吞并段的跨度        }         // 压入当前价格及累积跨度        stack.push(new int[]{price, span});        return span;    }}

为什么是 <= 而不是 <? 相等也要吞并——题目要求"小于或等于今天的价格",连续相等的价格应该合并在同一段里。

为什么 span 是累加栈顶的 [1] 而不是 +1? 因为栈顶 [1] 已经是那段打包好的跨度。比如栈顶是 {70, 2},说明 70 那天已经吞掉了 60(1 天),现在是 2 天。今天 75 来吞它,直接 +2,而不是只 +1。

四、过程图解

以下用 [100, 80, 60, 70, 60, 75, 85] 逐步演示栈的变化:

单调栈处理股票价格跨度全过程

配合上图,走一遍关键步骤:

Day 1: price=100
栈空,直接压入 {100, 1},返回 1。
栈:[100|1]

Day 2: price=80
栈顶 100 > 80,不弹出。压入 {80, 1},返回 1。
栈:[100|1, 80|1]

Day 3: price=60
栈顶 80 > 60,不弹出。压入 {60, 1},返回 1。
栈:[100|1, 80|1, 60|1]

Day 4: price=70
栈顶 60 ≤ 70,弹出 {60, 1},span=1+1=2。
新栈顶 80 > 70,停止。压入 {70, 2},返回 2。
栈:[100|1, 80|1, 70|2]

Day 5: price=60
栈顶 70 > 60,不弹出。压入 {60, 1},返回 1。
栈:[100|1, 80|1, 70|2, 60|1]

Day 6: price=75
栈顶 60 ≤ 75,弹出 {60, 1},span=1+1=2。
新栈顶 70 ≤ 75,弹出 {70, 2},span=2+2=4。
新栈顶 80 > 75,停止。压入 {75, 4},返回 4。
栈:[100|1, 80|1, 75|4]

注意这里:弹出 {70, 2} 时直接加了 2,一次性"吞"掉了 70 和它之前吃掉的 60,而不是再回头检查 60。

Day 7: price=85
栈顶 75 ≤ 85,弹出 {75, 4},span=1+4=5。
新栈顶 80 ≤ 85,弹出 {80, 1},span=5+1=6。
新栈顶 100 > 85,停止。压入 {85, 6},返回 6。
栈:[100|1, 85|6]

最终栈中只剩两个元素:从未被超越的 100,以及吞掉了后面一切的 85。


本文由 AgentPlanFlow 生成

觉得不错?给作者点个赞吧

分类:题解

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

Leetcode 901:单调栈 · Yinchen 的博客