急診室的「檢傷分類」:為什麼「堆積」(Heap) 是電腦世界最高效的 VIP 通道?

更新 發佈閱讀 15 分鐘

你走進一間醫院的急診室。這裡的規則,和你在銀行排隊的「佇列 (Queue)」完全不同。在銀行,你遵守「先進先出 (FIFO)」的公平原則。但在急診室,如果一位胸口中槍的病患,比一位只是手指割傷的病患「晚到」,護理師會讓誰先進去?答案不言而喻。這就是「優先佇列 (Priority Queue, PQ)」的核心精神:它不是「先進先出」,而是「最重要者先出」。

這個「急診室」結構,在電腦世界中無所不在。你的作業系統,必須在數百個「等待中」的程式裡,決定「下一個」該把 CPU 資源給誰?(通常是「優先級最高」的那個)。Google Maps 在規劃路線時,必須在數百萬條「可能的」路徑中,不斷地問:「目前已知『最短』的路徑是哪一條?」。這些系統,都不在乎「公平」,它們只在乎「最優」。

因此,一個「優先佇列」的基本操作,和「佇列」或「堆疊」完全不同。它主要提供三種服務:

  1. Insert (插入):一位新病患抵達急診室。
  2. GetMax (取得最大值):護理師「查看」目前候診區中,病情最嚴重的病患是誰。
  3. ExtractMax (取出最大值):護理師「呼叫」這位最嚴重的病患,將他「移出」候診區,送進手術室。 我們的挑戰,就是打造一個資料結構,讓這三種操作——特別是 GetMax 和 ExtractMax——變得「盡可能地快」。

為什麼我們「舊的」武器都失效了?

在我們發明新工具前,讓我們試試看用「舊的」武器來打造這間「急診室」,看看它們的效率有多慘:

方法一:使用「未排序的陣列 (Unsorted Array)」。這就像一個「雜亂」的候診區。Insert (新病患進來) 非常快,O(1),你只要隨便找張椅子坐下就好。但是,當醫生要「取出」最嚴重的病患時 (ExtractMax),就成了一場災難。護理師必須「逐一」詢問候診區的「每一個人」,比較他們的病情,才能找到最嚴重的那位。這個 O(n) 的「線性掃描」,在病患眾多時是致命的緩慢。

方法二:使用「已排序的陣列 (Sorted Array)」。這就像一個「紀律嚴明」的候診區,病患「永遠」按照「嚴重程度」排排坐。ExtractMax (取出最嚴重的) 變得無比神速,O(1),護理師只要從「最嚴重」的那一頭帶走病患就好。但是,Insert (新病患進來) 卻成了 O(n) 的惡夢。當一位「極度嚴重」的新病患衝進來時,為了把他安插到「隊伍最前端」,所有「排在他後面」的、病情較輕的病患,都必須「集體往後挪動一個座位」,這又回到了我們最痛恨的「集體位移」。

方法三:使用「自平衡二元搜尋樹 (BST)」(例如 AVL 樹)。這是我們目前最強的「全能」武器。Insert 是 O(log n)。ExtractMax(也就是去「搜尋」樹中「最右邊」的那個節點,然後「刪除」它)也是 O(log n)。這已經「非常、非常好了」!我們的急診室,所有操作都可以在「對數時間」內完成。但是... 我們還能「更快」嗎?我們真的需要 O(log n) 這麼久,才能「看」一眼誰最嚴重 (GetMax) 嗎?我們能不能讓 GetMax 變成 O(1),也就是「一瞬間」呢?[00:13:00]

登峰造極的「堆積」(Heap):O(1) 的王者

為了這個「O(1) 的 GetMax」,我們需要一個全新的、特化的結構。這,就是「二元堆積 (Binary Heap)」。它在「外觀」上,和「二元樹」非常相似,但它「不是」一棵「二元搜尋樹」。它不遵守「左小右大」的黃金法則。相反,它遵守兩條「更嚴格」的戒律:

戒律一:「結構屬性 (Structure Property)」。一棵「二元堆積」必須是一棵「完全二元樹 (Complete Binary Tree)」。這個詞的意思是,這棵樹的「每一層」都必須被「填滿」,唯一的例外是「最後一層」;而「最後一層」也必須「由左至右、緊密地」填滿,中間「不准有任何空隙」。這條戒律,確保了這棵樹「永遠」是「又寬又矮」的完美金字塔,它的高度「永遠」被保證在 O(log n)。

戒律二:「堆積屬性 (Heap Property)」。這是它「效能」的來源。如果我們要做一個「最大堆積 (Max Heap)」(用來找最大值),這條戒律就是:「樹中的『任何』一個父節點 (Parent),其值都『必須』大於或等於它『兩個』子節點 (Children) 的值。」[00:18:20] 這就像一個「家族」,爸爸永遠比兒子強壯。

這條「父 > 子」的戒律,在整棵樹中「層層遞迴」下去,會產生一個無比美妙的「必然」結果:整棵樹中「最大」的那個值,「必定」會一路「過關斬將」,被「推」到整棵樹的「頂端」—— 也就是「根節點 (Root)」。因此,當護理師要 GetMax (查看誰最嚴重) 時,她需要做什麼?「什麼都不用做」。她只需要「抬頭看一眼」根節點是誰,就完成了。GetMax 的效率,達成了我們夢寐以求的 O(1)!

史上最「節省」的樹:用「陣列」來儲存「樹」

我們有了一個 O(1) 的 GetMax,但要如何「實作」它呢?我們當然可以使用「節點」和「指標」(parent, left, right)來打造這棵樹。但是,還記得嗎?「指標」會導致資料「隨機散落」在記憶體中,造成「快取錯失 (Cache Miss)」,效率不佳。這時,「戒律一」(完全二元樹)的「天才伏筆」就顯現了。

正是因為這棵樹被「強制」規定為「完全、無空隙」的,所以它的「形狀」是「完全可預測的」。我們可以把這棵「樹」給「壓扁」,完美地「儲存」在一個「陣列 (Array)」中,而「不需要」任何指標! 儲存的方式,就是「一層一層、由左至右」地放入陣列。

這個「壓扁」的過程,創造了一套「神奇的數學公式」:對於陣列中「任意」一個索引為 i 的節點:

  • 它的「左子節點」必定在:2i + 1
  • 它的「右子節點」必定在:2i + 2
  • 它的「父節點」必定在:floor((i - 1) / 2) 這是一個「革命性」的實作!我們只用了一個「陣列」,就同時獲得了「樹」的 O(log n) 高度結構,以及「陣列」的「記憶體連續性」(這意味著「快取效率」極高)[00:23:50]。這幾乎是電腦科學中,最完美的「魚與熊掌兼得」的案例之一。

往上游 (Swim Up)!「新增」的平衡之舞

我們有了 O(1) 的 GetMax,但我們還需要處理 Insert。我們要如何「新增」一個值(例如 100)到這台「陣列樹」中,同時「維持」這兩條神聖的戒律?

步驟一:維持「結構屬性」。戒律一(完全二元樹)規定,新節點「必須」放在「最後一層」的「下一個空位」。在我們的「陣列」實作中,這簡直太簡單了:我們只需要把 100 push_back 到「陣列的尾端」 就好。O(1) 完成。

步驟二:修復「堆積屬性」。但是,這個新來的 100,它的「父節點」可能是 30。這就違反了「父 > 子」的戒律二!我們必須「修復」它。這個修復的動作,被生動地稱為「往上游 (Swim Up)」(或 Bubble Up)。

這個新節點 (100) 會開始「質疑」它的父節點 (30):「你比我小,憑什麼當我爸?」於是,它們「交換 (Swap)」位置。現在 100 到了 30 的位置,它會「繼續」質疑它的「新父節點」(例如 70)。「你還是比我小!」於是,它們「再次交換」。這個節點,就像一個「溺水者」拼命往上游,不斷地「挑戰」並「交換」它的父節點,直到它 (a) 遇到一個「比它大」的父節點(找到了它的「長輩」),或者 (b) 它一路「游」到了「根節點」,成為了新的「王」。因為樹的高度是 O(log n),所以這趟「往上游」的路徑,其成本最多就是 O(log n)。

向下沉 (Sink Down)!「取出」的王位交接

Insert 搞定了。現在,輪到最複雜的 ExtractMax (取出最大值)。我們知道,最大值「永遠」在「根節點」(陣列的 index 0)。

步驟一:移走「國王」。我們把 index 0 的值(例如 200)取走。這時,index 0 出現了一個「王座的空缺」。更糟的是,我們的「陣列」index 0 變成空的,這破壞了「戒律一」的「連續性」。

步驟二:「冒牌者」登基。為了「立刻」填補這個空缺,並維持「完全二元樹」的結構(陣列中不能有洞),我們使出了一個「驚人」的妙計:我們跑到「陣列的尾端」,抓起「最後一個」節點(例如 15)——那個「最微不足道」的樹葉——然後「瞬間移動」它,把它「直接丟到 index 0 的王座上」。

步驟三:修復「堆積屬性」。現在,結構是完整了(陣列是連續的),但「堆積屬性」被「徹底粉碎」。這個「冒牌國王」(15) 坐在王座上,但它的「孩子們」可能是 180 和 160,都比它大!它必須「退位」。這個動作,被稱為「向下沉 (Sink Down)」(或 Trickle Down)。

這個「冒牌者」(15) 會「往下看」,比較它的「兩個孩子」(180 和 160),找出「最強的那個」(180)。然後,它和「最強者」交換 (Swap) 位置。現在 15 下沉到了 180 的位置,它「繼續」往下看,和它的「新孩子」中「最強的那個」交換。它就這樣一路「下沉」,直到它 (a) 遇到一個位置,它的「兩個孩子」都比它小(它找到了「適合它的階層」),或者 (b) 它「沉」到了樹葉。這趟「向下沉」的路徑,成本最多也是 O(log n)。

排序的「副作用」:認識「堆積排序」(HeapSort)

我們現在有了一台「完美」的急診室機器:

  • GetMax (偷看):O(1)
  • Insert (新病患):O(log n)
  • ExtractMax (送入開刀):O(log n)

這時,電腦科學家又產生了一個「副作用」般的靈感:如果我有一堆「未排序」的數字,我能不能用這台機器來「排序」它們?[00:39:00] 當然可以!這就是大名鼎鼎的「堆積排序 (HeapSort)」。演算法有兩個階段:

階段一:「建立堆積 (BuildHeap)」。你把所有 n 個未排序的數字,全都 Insert 到一個「最大堆積」裡。這需要 n 次「往上游」,所以總成本是 O(n log n)。 階段二:「依序取出」。你呼叫 ExtractMax n 次。你第一次拿出來的,「保證」是最大的;第二次拿出來的,「保證」是第二大的... 這 n 次「向下沉」,總成本也是 O(n log n)。 於是,你得到了一個 O(n log n) 的高效排序演算法!

但教授接著揭示了一個「隱藏版」的優化。在「階段一」,我們「不需要」真的呼叫 Insert n 次。有一個「更偷懶」的「O(n) 建堆法」[00:42:30]:你先把 n 個數字「隨便」丟進陣列裡(此時它完全是個「錯誤」的堆積)。然後,你「從後往前」(從最後一個「非樹葉」節點開始),對「每一個」節點,都執行一次「向下沉 (Sink Down)」[00:43:00]。這個「由下而上、逐層修復」的過程,透過精妙的數學證明,其「總成本」竟然不是 O(n log n),而是神奇的 O(n)![00:46:00]

終極應用:Dijkstra 與 Google Maps 的「心臟」

「堆積」最強大的應用,還不是排序,而是作為「演算法的引擎」。這其中最經典的,就是「Dijkstra 演算法」[00:50:00],也就是 Google Maps「路線規劃」的核心。Dijkstra 演算法的目標,是找出從「A 點」到「所有其他點」的「最短路徑」。

想像一下,演算法就像一個「探險家」,站在 A 點。它的「優先佇列」(PQ) 裡,放著所有它「聽說過、但還沒親自造訪」的地點(節點),而這些地點的「優先級」,就是「目前已知的、到該地點的最短距離」。演算法的「心跳」就是一個迴圈:

  1. ExtractMin(這是「最小堆積」):從 PQ 中,取出「目前已知距離最近」的那個地點(U)。
  2. 「踏上」U 點,並「宣告」:我到 U 的最短路徑「已確定」。
  3. 接著,U 點的探險家,會望向它「所有」的「鄰居」(V),並「更新」資訊。

這「更新」的一步,是 PQ 的關鍵所在。探險家在 U 點喊道:「嘿!V!我發現一條『經過我』到你那邊的新路徑(距離(A->U) + 距離(U->V)),這條路徑比你『目前已知』的路徑還要『短』耶!」V 點一聽,大喜過望,它在 PQ 中的「優先級」(距離)必須被「更新」(變得更短、更優先)。這個「在 PQ 中,調高一個節點的優先級」的特殊操作,就叫做「DecreaseKey」。在「二元堆積」中,這其實就是一次「往上游 (Swim Up)」—— 成本依然是 O(log n)。ExtractMin 和 DecreaseKey,這兩個 O(log n) 的操作,就是 Dijkstra 演算法的「心臟」,支撐著全球的導航系統。

「專精」的極致,與無盡的優化

從一個簡單的「急診室」需求出發,最終打造出了「堆積」這個「特化的天才」。它「犧牲」了我們習以為常的「一般搜尋」能力(在堆積中「搜尋 7」是 O(n) 的災難),以換取在「優先級搜尋」上的「極致效能」(GetMax 是 O(1))。

它更是一個「實作」上的奇蹟。它用「陣列」的身軀,承載了「樹」的靈魂,將 O(log n) 的高度與「快取友善」的物理特性合而為一。「往上游」和「向下沉」這兩個簡單的「交換」動作,成為了維護這套「脆弱平衡」的優雅舞蹈,並催生了「堆積排序」和「Dijkstra 演算法」等無數的偉大應用。

而這,還不是終點。對於 Dijkstra 這種「極度依賴」DecreaseKey 的演算法,電腦科學家甚至發明了「二項堆積 (Binomial Heap)」和「費波那契堆積 (Fibonacci Heap)」。後者運用了「平攤分析」的魔術,將 Insert 和 DecreaseKey 的「平均」成本,壓低到了不可思議的 O(1) !這場「效率」的聖杯之戰,永無止盡。但「二元堆積」,無疑是這場戰役中,最經典、最實用、也最優雅的一座豐碑。


留言
avatar-img
留言分享你的想法!
avatar-img
資訊科學的小筆記
0會員
17內容數
我上AI相關課程的心得
2025/11/05
本文深入探討了三種重要的平衡搜尋樹結構:AVL 樹、紅黑樹和 B 樹。首先介紹了 AVL 樹作為效率標竿,強調其嚴格的平衡機制與 O(log n) 的搜尋、插入、刪除時間複雜度。接著引入紅黑樹,作為 AVL 樹的務實妥協,討論其透過顏色屬性而非絕對高度來維持平衡,使其在插入和刪除操作上成本更低。
2025/11/05
本文深入探討了三種重要的平衡搜尋樹結構:AVL 樹、紅黑樹和 B 樹。首先介紹了 AVL 樹作為效率標竿,強調其嚴格的平衡機制與 O(log n) 的搜尋、插入、刪除時間複雜度。接著引入紅黑樹,作為 AVL 樹的務實妥協,討論其透過顏色屬性而非絕對高度來維持平衡,使其在插入和刪除操作上成本更低。
2025/11/05
這篇文章深入探討了二元搜尋樹 (BST) 的效能瓶頸,特別是在資料依序插入時會退化成鏈結串列的 O(n) 複雜度問題。文章接著介紹了 AVL 樹,這是歷史上第一個自平衡二元搜尋樹,由 Adelson-Velsky 和 Landis 發明。
2025/11/05
這篇文章深入探討了二元搜尋樹 (BST) 的效能瓶頸,特別是在資料依序插入時會退化成鏈結串列的 O(n) 複雜度問題。文章接著介紹了 AVL 樹,這是歷史上第一個自平衡二元搜尋樹,由 Adelson-Velsky 和 Landis 發明。
2025/11/05
告別一維世界的線性束縛,踏入二元搜尋樹 (BST) 的空間領域!本文從族譜的直觀比喻出發,帶您一步步理解節點、根、父子關係等 BST 核心概念。深入剖析 BST 的「黃金法則」,揭示 O(log n) 閃電搜尋的原理,以及搜尋與新增操作的優雅整合。
Thumbnail
2025/11/05
告別一維世界的線性束縛,踏入二元搜尋樹 (BST) 的空間領域!本文從族譜的直觀比喻出發,帶您一步步理解節點、根、父子關係等 BST 核心概念。深入剖析 BST 的「黃金法則」,揭示 O(log n) 閃電搜尋的原理,以及搜尋與新增操作的優雅整合。
Thumbnail
看更多
你可能也想看
Thumbnail
搬家不只添購必需品,更能透過蝦皮分潤計畫賺取零用金!本文分享近期搬家時添購的各種實用好物,包含多功能工作桌、電競椅、氣炸烤箱、收納神器等,並詳述如何透過蝦皮雙 11 活動聰明購物、善用優惠,同時利用分潤機制將敗家行為轉化為被動收入,推薦給想聰明消費又想賺額外收入的你!
Thumbnail
搬家不只添購必需品,更能透過蝦皮分潤計畫賺取零用金!本文分享近期搬家時添購的各種實用好物,包含多功能工作桌、電競椅、氣炸烤箱、收納神器等,並詳述如何透過蝦皮雙 11 活動聰明購物、善用優惠,同時利用分潤機制將敗家行為轉化為被動收入,推薦給想聰明消費又想賺額外收入的你!
Thumbnail
貓奴每月進貢的時間又來啦! 身為專業貢品官,我從蝦皮搜尋各種零食,只為取悅家中三位貓主子!結果究竟會是龍心大悅,亦或是冷眼相待,就讓我們繼續看下去~
Thumbnail
貓奴每月進貢的時間又來啦! 身為專業貢品官,我從蝦皮搜尋各種零食,只為取悅家中三位貓主子!結果究竟會是龍心大悅,亦或是冷眼相待,就讓我們繼續看下去~
Thumbnail
期末考結束了!這學期我負責開了大二必修課:資料結構(Data Structure),雖然作業、小考都是由TA(課程助理)幫我批改,不過期中期末考都是我自己把所有的考卷改完的。總共大約是將近150份的考卷,有兩個班。而期末考的內容,其實基本上我把大部分的規則、程式碼都寫在題目裡面或者放在附錄讓同學可以
Thumbnail
期末考結束了!這學期我負責開了大二必修課:資料結構(Data Structure),雖然作業、小考都是由TA(課程助理)幫我批改,不過期中期末考都是我自己把所有的考卷改完的。總共大約是將近150份的考卷,有兩個班。而期末考的內容,其實基本上我把大部分的規則、程式碼都寫在題目裡面或者放在附錄讓同學可以
Thumbnail
請使用 C 或 Java 語言寫一副程式,此副程式對一個長度為 10 的整數陣列 A[0:9],最多花費 15 次的數值比較運算,尋找陣列中的最小值及最大值,並分別存入 Min 及 Max。(注意:請加註解說明程式碼寫法) 【103.關務】
Thumbnail
請使用 C 或 Java 語言寫一副程式,此副程式對一個長度為 10 的整數陣列 A[0:9],最多花費 15 次的數值比較運算,尋找陣列中的最小值及最大值,並分別存入 Min 及 Max。(注意:請加註解說明程式碼寫法) 【103.關務】
Thumbnail
這篇文章說明演算法的定義、特徵以及一個有趣的小知識。演算法被定義為解決問題的流程,並以機車故障為例說明。文章也列出演算法的五個特徵:輸入、有限性、明確性、有效性及輸出。最後,文章提及世界上公認的第一個演算法是歐幾裡德演算法。
Thumbnail
這篇文章說明演算法的定義、特徵以及一個有趣的小知識。演算法被定義為解決問題的流程,並以機車故障為例說明。文章也列出演算法的五個特徵:輸入、有限性、明確性、有效性及輸出。最後,文章提及世界上公認的第一個演算法是歐幾裡德演算法。
Thumbnail
上次我們提到了演算法(algorithm),它是一種解決問題的方式。但演算法只是資料結構與演算法(Data Structures and Algorithms, DSA)這個領域的一部分。今天,我們要進一步探索這個主題,了解它的核心概念。 什麼是資料結構與演算法呢?簡單來說,資料結構是用來組織和存
Thumbnail
上次我們提到了演算法(algorithm),它是一種解決問題的方式。但演算法只是資料結構與演算法(Data Structures and Algorithms, DSA)這個領域的一部分。今天,我們要進一步探索這個主題,了解它的核心概念。 什麼是資料結構與演算法呢?簡單來說,資料結構是用來組織和存
Thumbnail
在資料結構與演算法裡, 最簡單的線性資料結構除了array之外就是linked list鏈結串列了。 Linked list又有分為單向Singly linked list 和雙向Doubly linked list
Thumbnail
在資料結構與演算法裡, 最簡單的線性資料結構除了array之外就是linked list鏈結串列了。 Linked list又有分為單向Singly linked list 和雙向Doubly linked list
Thumbnail
從範例學python的目標讀者: 針對剛進入的初學者,想學習Python語言。 有基礎本數學邏輯基礎即可。 從小遊戲學python的目標讀者: 針對已經有經驗的C/C++, Python, 或其他有程式基礎的讀者。 想實作一些小專案,從實做中學習如何分析需求、元件分拆、到底層實作
Thumbnail
從範例學python的目標讀者: 針對剛進入的初學者,想學習Python語言。 有基礎本數學邏輯基礎即可。 從小遊戲學python的目標讀者: 針對已經有經驗的C/C++, Python, 或其他有程式基礎的讀者。 想實作一些小專案,從實做中學習如何分析需求、元件分拆、到底層實作
Thumbnail
在資料結構與演算法裡, 最簡單的線性資料結構除了list之外就是linked list鏈結串列了。 Linked list又有分為單向Singly linked list 和雙向Doubly linked list 在這篇文章,會從最基礎的Singly linked list開始講起。 定義
Thumbnail
在資料結構與演算法裡, 最簡單的線性資料結構除了list之外就是linked list鏈結串列了。 Linked list又有分為單向Singly linked list 和雙向Doubly linked list 在這篇文章,會從最基礎的Singly linked list開始講起。 定義
Thumbnail
本文將深入探討鏈表的核心概念,使用 JavaScript 來說明如何實現和操作鏈表(Linked List),包括 append、prepend、remove、find 和 reverse 等五大方法。
Thumbnail
本文將深入探討鏈表的核心概念,使用 JavaScript 來說明如何實現和操作鏈表(Linked List),包括 append、prepend、remove、find 和 reverse 等五大方法。
Thumbnail
這道題你一定會解,但你知道怎麼把迴圈改寫成「迭代」嗎?這招學起來!
Thumbnail
這道題你一定會解,但你知道怎麼把迴圈改寫成「迭代」嗎?這招學起來!
Thumbnail
演算法學習路徑圖 這次我們將精確定位出,在整個演算法學習中,我們所站立的位置;了解資料結構與演算法的定義後,拿到我們在這個世界中的方位,就能「有根有據」的展開學習路徑,建立「系統架構化」的知識體系。 不論學習什麼,明確的「定位」出自己在整個學習藍圖中的位置,是非常重要的。軟體世界要學的何其多,光是
Thumbnail
演算法學習路徑圖 這次我們將精確定位出,在整個演算法學習中,我們所站立的位置;了解資料結構與演算法的定義後,拿到我們在這個世界中的方位,就能「有根有據」的展開學習路徑,建立「系統架構化」的知識體系。 不論學習什麼,明確的「定位」出自己在整個學習藍圖中的位置,是非常重要的。軟體世界要學的何其多,光是
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News