Online Stock Span

題目描述

設計一個類別 StockSpanner,每次呼叫 next(price) 回傳包含今天在內的連續天數中,股價都 ≤ 今天的天數。

StockSpanner sp = new StockSpanner();
sp.next(100); // 1
sp.next(80);  // 1
sp.next(60);  // 1
sp.next(70);  // 2 (60, 70)
sp.next(60);  // 1
sp.next(75);  // 4 (60, 70, 60, 75... 等等回到 80 停)
sp.next(85);  // 6

解題思路

1

Stack 存配對

Stack 中每個元素是 (price, span) 配對,維護從底到頂嚴格遞減的價格序列

2

新價格進來

初始 span = 1(至少包含今天自己),例如價格 75 進來,span 從 1 開始

3

合併較小的

Stack 頂端的 price ≤ 當前 price → 彈出並累加其 span。例如彈出 (60,1) 後 span 變成 2,再彈出 (70,2) 後 span 變成 4

4

壓入 Stack

將 (price, 累積的 span) 壓入 Stack,例如壓入 (75, 4)

5

回傳 span

回傳當前的 span 值作為答案

程式碼

C#

csharp
public class StockSpanner {
    private Stack<(int price, int span)> stack;
    
    public StockSpanner() {
        stack = new Stack<(int, int)>();
    }
    
    public int Next(int price) {
        int span = 1;
        // 合併所有 <= 當前價格的記錄
        while (stack.Count > 0 && stack.Peek().price <= price) {
            span += stack.Pop().span;
        }
        stack.Push((price, span));
        return span;
    }
}

TypeScript

typescript
class StockSpanner {
    private stack: [number, number][]; // [price, span]
    
    constructor() {
        this.stack = [];
    }
    
    next(price: number): number {
        let span = 1;
        while (this.stack.length > 0 && this.stack[this.stack.length - 1][0] <= price) {
            span += this.stack.pop()![1];
        }
        this.stack.push([price, span]);
        return span;
    }
}

Python

python
class StockSpanner:
    def __init__(self):
        self.stack = []  # [(price, span)]
    
    def next(self, price: int) -> int:
        span = 1
        # 合併所有 <= 當前價格的歷史記錄
        while self.stack and self.stack[-1][0] <= price:
            span += self.stack.pop()[1]
        self.stack.append((price, span))
        return span

複雜度分析

| | Time | Space | |--|------|-------| | 每次 next() | 均攤 O(1) | O(n) |

邊界情況

圖解

新價格 75 進來
比較頂端
Stack: [(100,1),(80,1),(60,1),(70,2),(60,1)]
60<=75
彈出 (60,1) → span=2
70<=75
彈出 (70,2) → span=4
80>75 停止
彈出 (60,1)... 不對 70>75? 不是
壓入 (75, 4)

圖表展示了價格 75 進來時的合併過程:先彈出 (60,1) 再彈出 (70,2),累計 span=1+1+2=4。 遇到 (80,1) 時 80 > 75 停止合併,最終壓入 (75,4)。被彈出的記錄已被 75 的 span 吸收。

進階觀點

💡面試追問

Q: 為什麼是均攤 O(1)? A: 每個元素最多被 push 一次和 pop 一次。n 次 next() 的總操作數 ≤ 2n(n 次 push + 最多 n 次 pop),均攤每次操作 O(2n/n) = O(1)。雖然單次 next() 最壞可能 pop 掉所有元素(O(n)),但那些元素之後不會再被 pop,總量固定。

Q: Stack 中存 span 而不是逐一記錄每天的好處? A: 合併後的 span 保留了「被吞掉的天數」資訊。如果只存 price,被彈出後就無法知道它代表了幾天,必須逐天回溯,單次操作就不是 O(1) 了。

Q: 和 739. Daily Temperatures 的對比? A: Daily Temperatures 找「下一個更大的」(Next Greater Element),本題找「前面連續不大於的跨度」(Previous Greater Element)。兩者都用單調遞減 Stack,是同一概念的正反方向應用。

Q: 如果要支援「撤銷上一次操作」怎麼做? A: 需要額外記錄每次 next() 被彈出的元素列表。撤銷時把它們重新 push 回 Stack,並移除最後壓入的元素。相當於一個 undo stack 疊加在原有結構上。

理解測驗

🤔 為什麼 Stack 中要存 (price, span) 而不只是 price?

🤔 如果連續輸入遞減的價格 [100, 90, 80, 70],Stack 的狀態是什麼?

你可能也想看

739. Daily Temperatures回到目錄 →

按 ← → 鍵切換課程