單調堆疊與單調佇列
讓堆疊或佇列裡的值一直保持遞增或遞減:每個元素只進出一次,就能在 O(n) 內找出「下一個比我大的」或「每個視窗的最大值」。
題目
輸入
1/25
堆疊(底 → 頂)[0] 73
長條下方的數字是那一天的答案:再過幾天會更熱。
目前這格比較中被移除在堆疊裡
把第 0 天放上去等更熱的一天。堆疊維持「越上面越冷」。
目前工作量:1(整趟 24;暴力法 11)
亮起來的是這一步執行的程式碼
// For each day, how many days until a warmer one (0 if never).function dailyTemperatures(temps: number[]): number[] { const answer = new Array(temps.length).fill(0); const stack: number[] = []; // days still waiting, temps falling for (let i = 0; i < temps.length; i++) { while (stack.length && temps[stack[stack.length - 1]] < temps[i]) { const j = stack.pop()!; answer[j] = i - j; } stack.push(i); } return answer;} // The maximum of every window of k consecutive values.function slidingMax(nums: number[], k: number): number[] { const deque = new Array<number>(Math.min(k, nums.length)); // circular buffer, O(k) let head = 0, size = 0; const at = (offset: number) => deque[(head + offset) % deque.length]; const maxima: number[] = []; for (let i = 0; i < nums.length; i++) { if (size && at(0) <= i - k) { head = (head + 1) % deque.length; size--; } while (size && nums[at(size - 1)] <= nums[i]) { size--; } deque[(head + size) % deque.length] = i; size++; if (i >= k - 1) maxima.push(nums[at(0)]); } return maxima;}模型假設與範圍
- Big O 與實測步數以頁面列出的基本操作為單位;視覺化的快照、畫圖、程式高亮和輸出成本另計,不是執行時間 benchmark。
- 視窗大小 k 須是正整數;環形 deque 輔助空間 O(k),輸出另佔 O(n)。錄下每步的陣列快照會增加時間與記憶體。
什麼時候用
- 題目問「每個元素左邊或右邊第一個比它大(或小)的是誰」:下一個更熱的一天、下一個更高的股價、柱狀圖裡每根柱子能往兩邊延伸多遠。看到這種描述就想單調堆疊。
- 固定長度的視窗一路滑過去,每個位置都要最大值或最小值:用單調佇列。視窗長度會變、但只往右縮放的題目(例如動態規劃的轉移)也適用。
和其他主題的關係
- 被這些用到
- 演算法 · 考題:接雨水
時間與空間複雜度(Big O)
| 操作 | 平均 | 最差 |
|---|---|---|
| 每日溫度(單調堆疊) 每天最多進堆疊一次、出堆疊一次 | O(n) | O(n) |
| 滑動視窗最大值(單調佇列) 跟視窗大小 k 無關 | O(n) | O(n) |
| 暴力法 | O(n²) / O(nk) | O(n²) / O(nk) |
空間:O(n),堆疊最多放 n 個;佇列最多放 k 個
Big O 實測:n 變大時步數怎麼長
數的是:總工作量(比較、放入、取出的次數)
| Big O | n = 250 | n = 500 | n = 1,000 | n = 2,000 | 成長倍數:實測(理論) | |
|---|---|---|---|---|---|---|
| 每日溫度:單調堆疊 | O(n) | 499 | 999 | 1,999 | 3,999 | ×8.0 (×8.0) |
| 每日溫度:往後逐天找 | O(n²) | 31,125 | 124,750 | 499,500 | 1,999,000 | ×64 (×64) |
| 滑動視窗:單調佇列 | O(n) | 967 | 1,956 | 3,937 | 7,910 | ×8.2 (×8.0) |
| 滑動視窗:每個視窗重掃 | O(n²) | 5,424 | 22,099 | 89,199 | 358,399 | ×66 (×64) |
視窗 k = n / 10,所以重掃的 O(nk) 在這裡就是 O(n²)。
和其他做法比
| n = 250 | n = 500 | n = 1,000 | n = 2,000 | |
|---|---|---|---|---|
| 每日溫度:單調堆疊 | 499 | 999 | 1,999 | 3,999 |
| 每日溫度:往後逐天找 | 31,125 | 124,750 | 499,500 | 1,999,000 |
| 滑動視窗:單調佇列 | 967 | 1,956 | 3,937 | 7,910 |
| 滑動視窗:每個視窗重掃 | 5,424 | 22,099 | 89,199 | 358,399 |
每日溫度用一路下降的溫度(暴力法最壞的情況:沒有一天等得到更熱的);滑動視窗用隨機數值、視窗 k = n / 10。堆疊和佇列裡每個索引最多進出各一次,所以 n 從 250 到 2,000 時工作量大約跟著翻 8 倍;暴力法則翻了 64 倍。
真實世界裡的它
- 股票的「股價跨度」(往前連續幾天都不比今天高)、柱狀圖最大矩形、接雨水。
- 監控系統的「過去 5 分鐘最高延遲」:資料一筆筆進來,用單調佇列隨時拿到視窗最大值。
取捨與陷阱
- 堆疊裡要放索引而不是值:答案通常要距離(i − j),而且相同的值需要分得出是哪一天。
- 等號決定相同值的去留:每日溫度要「嚴格更熱」才彈出(<);滑動視窗則是 <= 就丟,讓佇列裡保留最新的那個。寫反了,相同值的輸入就會出錯。
- 看起來是兩層迴圈不代表 O(n²):要用「每個元素最多進出一次」來算總量(攤銷分析),而不是用內層迴圈的最壞次數乘以外層。
LeetCode 練習
- 496.Next Greater Element IEasy下一個更大的元素:單調堆疊的入門題(在新分頁開啟 LeetCode)
- 739.Daily TemperaturesMedium往後找第一個更暖的日子(在新分頁開啟 LeetCode)
- 901.Online Stock SpanMedium往前找連續不比今天高的天數(在新分頁開啟 LeetCode)
- 239.Sliding Window MaximumHard單調佇列求每個視窗的最大值(在新分頁開啟 LeetCode)
- 84.Largest Rectangle in HistogramHard單調堆疊找每根柱子能延伸多遠(在新分頁開啟 LeetCode)