跳到主要內容

資料結構

資料怎麼排,決定了哪些操作便宜

主題 · 堆疊

堆疊

後進先出:只能在頂端放和拿。函式呼叫、括號配對、復原上一步,都是它。

示範
  1. 0A
  2. 1B
  3. 2C← top
  4. 3
剛放上去/正在讀的字元peek 看到的配不上已配置但沒用到

只能動最上面那一個:push 放上去、pop 拿下來、peek 只看不拿。

元素
3
容量
4
頂端
C
亮起來的是這一步執行的程式碼
class Stack<T> {
private data: T[] = new Array(4);
private size = 0;
push(value: T): void {
if (this.size === this.data.length) {
const bigger = new Array<T>(this.data.length * 2);
for (let i = 0; i < this.size; i++) bigger[i] = this.data[i];
this.data = bigger;
}
this.data[this.size++] = value;
}
pop(): T | undefined {
if (this.size === 0) return undefined;
return this.data[--this.size];
}
peek(): T | undefined {
return this.size === 0 ? undefined : this.data[this.size - 1];
}
isEmpty(): boolean {
return this.size === 0;
}
}
const PAIRS: Record<string, string> = { ")": "(", "]": "[", "}": "{" };
function isBalanced(text: string): boolean {
const stack = new Stack<string>();
for (const ch of text) {
if (ch === "(" || ch === "[" || ch === "{") {
stack.push(ch);
} else if (ch in PAIRS) {
if (stack.pop() !== PAIRS[ch]) return false;
}
}
return stack.isEmpty();
}

模型假設與範圍

  • Big O 與實測步數以頁面列出的基本操作為單位;視覺化的快照、畫圖、程式高亮和輸出成本另計,不是執行時間 benchmark。

什麼時候用

  • 後來的要先處理:函式呼叫、復原(undo)、瀏覽器的上一頁。
  • 配對與巢狀結構:括號、HTML 標籤、運算式求值。
  • 把遞迴改成迴圈:DFS(Depth-First Search,深度優先搜尋)用自己的堆疊取代呼叫堆疊,就不會因為遞迴太深而 stack overflow。

和其他主題的關係

由這些組成
動態陣列

語言內建的版本

Array<T>
操作寫法成本
推入stack.push(3)O(1) amortised
彈出stack.pop()O(1)
看頂端stack.at(-1)O(1)
是否為空stack.length === 0O(1)
const stack: number[] = [];
stack.push(1);
stack.push(2);
stack.push(3); // → 3
stack.at(-1); // → 3
stack[stack.length - 1]; // → 3
stack.pop(); // → 3
stack; // → [1, 2]
stack.pop(); // → 2
stack.pop(); // → 1
stack.length === 0; // → true
stack.pop(); // → undefined
stack.at(-1); // → undefined
// Balanced brackets: push openers, pop on a closer.
const pairs: Record<string, string> = { ")": "(", "]": "[", "}": "{" };
const balanced = (s: string) => {
const open: string[] = [];
for (const c of s) {
if (!(c in pairs)) open.push(c);
else if (open.pop() !== pairs[c]) return false;
}
return open.length === 0;
};
balanced("([]{})"); // → true
balanced("(]"); // → false

每個 → 後面的結果都是實際執行這段程式碼驗證過的。

  • JavaScript 沒有獨立的 stack 型別,陣列的 push/pop 就是。空陣列 pop() 不會丟錯,而是回傳 undefined,記得自己檢查 length。
  • 別用 shift()/unshift() 從頭端當堆疊,那是 O(n)。

時間與空間複雜度(Big O)

操作平均最差
push
均攤 O(1):只有擴容的那一次要複製
O(1)O(n)
popO(1)O(1)
peekO(1)O(1)
檢查長度 n 的括號字串
每個字元最多 push 或 pop 一次
O(n)O(n)

空間:O(n),括號檢查最差時,整串都是左括號

Big O 實測:n 變大時步數怎麼長

數的是:寫入的格子數,或括號檢查做的 push+pop 次數

Big On = 1,000n = 10,000n = 100,000成長倍數:實測(理論)
push(平均,含擴容)O(1)2.02.62.3×1.1 (×1.0)
檢查長度 n 的括號字串O(n)1,00010,000100,000×100 (×100)

真實世界裡的它

  • 每個執行緒的呼叫堆疊:區域變數和返回位址都疊在上面,遞迴太深就是 stack overflow。
  • 編輯器的復原和重做,是兩個堆疊互相倒來倒去。
  • JVM(Java Virtual Machine,Java 虛擬機)和 Python 的位元組碼都是堆疊機:a + b 會編譯成 push a、push b、add。

取捨與陷阱

  • 對空堆疊 pop 一定要處理:先檢查,或像這裡回傳 undefined/None/null。
  • JavaScript 陣列的 push/pop 就是堆疊,但 shift/unshift 是在開頭動,每次都要搬動整個陣列(見佇列)。
  • 只檢查「左右括號數量一樣」不夠:([)] 數量相等但交錯了,要靠堆疊才抓得到順序。

LeetCode 練習