品客、排隊與擲硬幣的高速公路:電腦如何思考「順序」?

更新 發佈閱讀 16 分鐘

在我們的數位生活中,「順序」似乎是理所當然的。你傳送的訊息,對方會照著順序接收;你按下「上一頁」,電腦會記得你「上一個」在哪裡;你聽音樂,總是可以「跳到下一首」。然而,在電腦的內心深處,維護「順序」是一場永無休止的戰爭。它必須不斷決定:這個新資訊,該放在哪裡?下一個該處理的,又是哪一個?

我們在前篇文章中,已經看到了這場戰爭的核心矛盾。一邊是「排序陣列 (Sorted Array)」:它像是一排座位「緊密相連」的電影院,你能用「二分搜尋法」(O(log n)) 瞬間找到中間的座位,但如果有人想「插入」到中間,O(n) 的「集體挪動」會讓所有人抓狂。另一邊是「鏈結串列 (Linked List)」:它像是「隨機散落」各處、僅靠「鎖鏈」相連的島嶼,要在中間「插入」一座新島嶼易如反掌 (O(1)),但要「搜尋」某座特定的島,O(n) 的「逐島遠征」同樣令人絕望。

這篇文章,我們將暫時放下這個「O(log n) 聖杯」的艱難挑戰。我們先回頭,學習兩個最簡單、最古老的「順序」模型。它們一個是「品客洋芋片」,一個是「排隊買午餐」。這兩種看似不起眼的結構,卻是支撐起你瀏覽器「上一頁」按鈕、乃至整個程式語言運作的隱形基石。最後,我們將帶著這些新知識,重返戰場,見證一個用「擲硬幣」來決定結構的瘋狂設計,是如何巧妙地解決了「陣列」與「串列」的世紀難題。

資訊的「品客」罐:堆疊 (Stack)

想像一個裝品客洋芋片的長筒罐。這個結構只有一個開口,就是「頂端」。當你放入洋芋片時,你只能從「頂端」放(這個動作,我們稱為 Push);當你取出洋芋片時,你也只能從「頂端」拿(這個動作,我們稱為 Pop)。你絕對不可能在不壓碎其他洋芋片的前提下,去拿中間或底部的那一片。這就是「堆疊」(Stack),一個電腦科學中最重要的「限制型」結構。

「堆疊」的整個精神,可以用四個字來概括:「後進先出 (Last In, First Out - LIFO)」。你最後放進去的那片洋芋片,永遠是你第一片拿出來的。這種「限制」聽起來很不方便,但在現實世界中,它完美地模擬了「中斷」與「恢復」的行為模式。例如,你正在撰寫一份報告(任務A),電話響了(任務B),你接起電話;在講電話時,外送員按了門鈴(任務C)。你的處理順序必定是:先處理門鈴 (C),然後回去講完電話 (B),最後才回到報告 (A)。你處理的順序,剛好就是你被打斷的「相反」順序。

這種 LIFO 結構,賦予了電腦一種「短期記憶」的能力。它允許電腦在處理一件複雜任務時,能「暫停」當前工作,去處理一個「更緊急」的子任務,並且在子任務完成後,能「完美地」回到它先前暫停的地方,彷彿什麼都沒發生過。這種「先處理完手上最新的事,再回頭處理舊事」的模式,正是「堆疊」的靈魂,也是它在電腦世界中無所不在的原因。

堆疊的「文法」:電腦如何檢查你的括號?

「堆疊」的第一個經典應用,就是扮演「文法警察」的角色。當你撰寫程式碼或數學公式時,你必須正確地使用括號,例如 ( [ { } ] )。電腦是如何判斷這串括號是「合法」的,而 ( [ ) ] 又是「非法」的呢?它不可能「通靈」或「讀懂」你的意圖,它需要一個嚴謹的演算法——而這個演算法的核心,就是一個「堆疊」。

想像一下,電腦拿到這串文字,開始「由左至右」讀取,它手上拿著一個空的「品客罐」(堆疊)。規則很簡單:當它看到一個「左括號」(例如 (, [, {),它就把它「推 (Push)」進堆疊裡。這就像在說:「我現在開始了一個新的『層次』,我要記住我是用什麼括號開始的。」所以,當它讀到 ( [ { 時,堆疊從底部到頂部,會依序是 (, [ , {。

接著,當它看到一個「右括號」(例如 })時,事情就有趣了。它會立刻從堆疊頂端「拿 (Pop)」出一個括號。然後它會檢查:我剛拿出來的 Pop(例如 {)和我現在看到的 },是不是一對?如果「是」,太好了,文法正確,我們繼續;如果「不是」(例如 Pop 出來的是 [,但你看到的是 }),或者你根本「Pop 不出東西」(代表堆疊是空的,你試圖「關閉」一個你從未「開啟」的括號),電腦就會立刻報錯:「文法錯誤!」。最後,當整個字串都讀完了,如果堆疊「剛好是空的」,就代表所有開啟的括號,都找到了它們配對的另一半,文法完美無誤。

堆疊的「記憶」:你的程式為何不會迷路?

如果說「檢查括號」只是小試身手,那麼「堆疊」的下一個應用,就是它在電腦世界中「至高無上」的職責:支撐起所有「函式呼叫」(Function Call)。這就是所謂的「呼叫堆疊 (Call Stack)」,它是你的程式碼得以運作的「大腦記憶區」。當你的主程式 (Main) 呼叫一個函式 (例如 Function_A) 時,電腦是如何「記得」在 A 結束後,要回到 Main 程式的哪一行繼續執行的?

答案是,當 Main 呼叫 A 時,電腦會把 A 的「任務資料」(包含它執行需要用到的變數、以及一個「返回地址」,標註著 Main 程式被中斷的哪一行)打包成一個「包裹」,然後 Push 到「呼叫堆疊」的頂端。現在,A 位於堆疊的頂端,CPU 就開始執行 A 的程式碼。如果 A 在執行過程中,又呼叫了 Function_B,電腦就再把 B 的「包裹」(以及 A 的返回地址)Push 到堆疊頂端。

CPU 永遠只處理「堆疊最頂端」的任務。所以它現在開始執行 B。當 B 執行完畢時,它的任務就完成了,於是電腦會把它從堆疊中 Pop 掉。Pop 掉之後,堆疊頂端露出來的是誰?是 A 的包裹。CPU 看到 A 的包裹,就高興地回去繼續執行 A 的任務。當 A 也完成了,它也被 Pop 掉,CPU 就回到了 Main。你一定聽過的「堆疊溢位 (Stack Overflow)」錯誤,意思就是你不斷地 Push 任務(例如一個無限的遞迴函式),卻從未 Pop,導致這個堆疊被塞爆,電腦的記憶體耗盡了。

資訊的「隊伍」:佇列 (Queue)

學會了「堆疊」,我們來認識它的兄弟:「佇列 (Queue)」。如果說「堆疊」是品客罐,那「佇列」就是排隊買午餐的隊伍。這個結構有「兩個」開口:一個是「隊伍尾端」,人們從這裡加入(這個動作,我們稱為 Enqueue);另一個是「隊伍前端」,人們從這裡離開(這個動作,我們稱為 Dequeue)。

「佇列」的精神,也可用四個字來概括:「先進先出 (First In, First Out - FIFO)」。你第一個來排隊,你就是第一個買到午餐的人。這是一種「公平」的結構。在電腦世界中,任何需要「排隊」、「等待服務」的場合,都是「佇列」的應用。你的印表機「等待列印」的檔案清單、你傳送聊天訊息的「等待伺服器接收」的順序、作業系統中「等待 CPU 處理」的任務們,它們全都是「佇列」。

這個結構確保了資源的「公平分配」。它不像「堆疊」那樣,會讓「最早來的」(在最底下的)等到天荒地老。在「佇列」中,只要你排隊了,你就可以合理地預期,在你前面的都處理完畢後,就一定會輪到你。這是一種基於「時間順序」的、我們人類社會最容易理解的處理模式。

佇列的「惡夢」:打造一條公平的隊伍

「佇列」的FIFO原則聽起來很棒,但要「實作」它,卻比「堆疊」要棘手得多。假設我們想用「動態陣列」來打造一個佇列。Enqueue(加入隊伍)很簡單,我們只要在陣列的「尾端」push_back 就好,這是我們上堂課學到的「平攤 O(1)」操作。

真正的惡夢,發生在 Dequeue(離開隊伍)。隊伍中第一個離開的人,是位於陣列「索引 0」的那個人。當他離開後,陣列 index 0 的位置就空出來了。我們總不能讓櫃台前面空一個洞吧?為了保持隊伍的「連續性」,我們被迫要請「後面所有的人」,全部往前移動一格。index 1 的人移到 index 0,index 2 的人移到 index 1...

這又回到了我們最痛恨的「O(n) 集體位移」!僅僅為了服務「一個人」,我們卻讓「所有人」都動了起來。這讓 Dequeue 變成了一個 O(n) 的昂貴操作。這條「公平」的隊伍,對電腦來說,一點也不「效率」。那怎麼辦?電腦科學家提供了兩種天才的解法:第一種是「環狀陣列 (Circular Array)」,我們想像陣列的「尾巴」和「頭」是相連的,像一條貪食蛇,我們用兩個指標 front 和 back 來追蹤隊伍的頭尾,這樣 Dequeue 時就不需要移動任何人,只需要移動 front 指標就好。

第二種,也是更優雅的解法,就是使用「鏈結串列 (Linked List)」。我們只需要維護兩個指標:一個指向「隊伍頭 (head)」,一個指向「隊伍尾 (tail)」。當有人要 Enqueue(加入),我們在 tail 的後面接上一個新節點,並更新 tail 指標,這是 O(1)。當有人要 Dequeue(離開),我們把 head 指向的節點移除,並更新 head 指標,這也是 O(1)。這是一個在 Enqueue 和 Dequeue 上,都達到了完美 O(1) 效率的佇列實作。

重返戰場:追求 O(log n) 的聖杯

好了,「堆疊」和「佇列」的「旁線任務」已經解完。我們學會了 LIFO 和 FIFO,也學會了如何用「鏈結串列」來優雅地實作出 O(1) 的佇列。現在,我們手上的工具更豐富了,是時候回到「主線任務」了。我們要重拾上堂課的「聖杯問題」:我們能否創造一個「單一」的資料結構,它必須同時滿足以下所有條件?

  1. Search (搜尋):像「二分搜尋」一樣快,O(log n)。
  2. Insert (新增):像「鏈結串列」一樣快,O(log n) (或 O(1))。
  3. Delete (刪除):也必須是 O(log n) (或 O(1))。

我們知道,「排序陣列」在「新增/刪除」上失敗了(O(n))。我們知道,「排序鏈結串列」在「搜尋」上失敗了(O(n))。我們需要一個全新的、革命性的想法。這時來了真正主角:一個叫做「跳躍串列 (Skip List)」的絕妙設計。

這個結構,是 William Pugh 在 1989 年發明的。它背後的思想是:「既然『鏈結串列』的搜尋很慢,是因為我們必須『一步一步』走,那為什麼我們不給它加幾條『快速通道』呢?」

資訊的高速公路:跳躍串列 (Skip List)

想像一下,「排序鏈結串列」就是一條「鄉間小路」。這條路上有 1, 2, 3, ... 到 n 號村莊(節點)。如果你要從 1 號村莊去 100 號村莊,你必須途經 2, 3, 4, ... 99 號,一步都不能少。這就是 O(n) 的「步行」搜尋。

現在,Pugh 提出了一個構想:我們在「鄉間小路」的「上方」,蓋一條「快速道路」。這條 L2 快速道路,並不停靠所有村莊,它只停靠「偶數」村莊(例如 ... 2, 4, 6, 8, ...)。現在,如果你要從 1 號去 100 號,你的策略變了:你先想辦法開上 L2 快速道路,然後一路飆車 ... 80 -> 82 -> ... -> 98 -> 100。這條路的長度,只有鄉間小路的一半。你的搜尋時間,也「減半」了。

Pugh 接著想:「既然蓋一條快速道路這麼有效,為什麼不蓋更多條呢?」於是他又在 L2 的上方,蓋了一條「L3 高速公路」,這條路更狂,它只停靠「每 4 個」村莊(例如 ... 4, 8, 12, ...)。然後再蓋一條「L4 洲際公路」,只停靠「每 8 個」村莊。以此類推,直到最上層的公路,可能只有「起點」和「終點」兩站。

攀登「跳躍」的樓梯:O(log n) 搜尋

這個「多層次」的公路系統,就是「跳躍串列」。現在,我們來看看「搜尋」是如何運作的,這也是它最精華的部分。假設,你要去 77 號村莊。你不會從 L1 鄉間小路開始,你會從「最高層」的 L4 洲際公路開始。你往前開,第一站是 64 號,下一站是 128 號。糟糕,「128 號」過頭 (Overshoot) 了!

你立刻在 64 號出口「降層」,來到 L3 高速公路。你從 64 號繼續開,下一站是 80 號。又「過頭」了!你再次「降層」,來到 L2 快速道路。你從 64 號繼續,下一站是 72 號,再下一站是 76 號,再下一站是 80 號。又「過頭」了!你從 76 號「降層」,來到 L1 鄉間小路。你從 76 號開始「步行」,下一站... 就是 77 號!你找到了!

這個「從最高層開始,一路搜尋,一旦過頭就降層」的「樓梯狀」搜尋路徑,它的效率有多高?如果你的資料總共有 n 筆,你的公路系統總共會有幾層?答案是 log n 層。你在每一層公路上,平均要開幾站?答案是「常數」站(平均不會超過 2 站)。因此,你的總搜尋時間,就是「層數」乘上「每層的站數」,也就是 O(log n) * O(1) = O(log n)。我們竟然在一個「鏈結串列」的變體上,實現了「二分搜尋」等級的 O(log n) 搜尋!

「擲硬幣」的魔術:隨機平衡的藝術

我們達成了 O(log n) 的搜尋。但別忘了「排序陣列」的教訓:如果 Insert (新增) 很慢,一切都是白搭。現在,我們要如何在「多層公路系統」中,「新增」一個 77 號村莊? 我們當然可以在最底層的 L1 鄉間小路,把 77 號加進去。但問題是:我們應不應該也幫 77 號,在 L2, L3, L4 蓋出口呢?

如果我們「不」蓋,那新加入的村莊,都會變成「只有鄉間小路能到」的偏鄉。久而久之,我們的公路系統會漸漸失效,搜尋效率又會退化回 O(n)。但如果我們「都」蓋,又該蓋幾層?這就是 Pugh 設計的「神來之筆」[00:51:30]。他說:「我們不要自己決定,我們讓『機率』來決定。」

當你要新增 77 號村莊時,你保證會把它放進 L1。然後,你擲一枚硬幣。如果是「正面」,你就「晉升 (Promote)」它,也幫它在 L2 蓋一個出口;然後你再擲一次硬幣,如果是「正面」,你再幫它蓋一個 L3 的出口... 你不斷地擲,直到擲出「反面」,你才停止晉升。

擁抱「隨機」,駕馭「效率」

「擲硬幣」?這聽起來像是瘋了,這根本不是嚴謹的科學!但這正是「跳躍串列」最偉大的地方:它是一個「機率型 (Probabilistic)」的資料結構。它不「保證」公路系統在「任何時刻」都是「完美平衡」的,但它透過「機率」,讓這個系統「在期望值上」達到了我們想要的平衡——平均來說,L2 會有 50% 的節點,L3 會有 25%,L4 會有 12.5%,這正是我們需要的多層金字塔結構!

因為「搜尋」是 O(log n),而「新增」節點時,我們也只需要 O(log n) 的時間(去找到插入點),然後花 O(log n) 的期望時間(擲硬幣、在各層插入指標),所以「跳躍串列」的 Search 和 Insert (以及 Delete),全部都是 O(log n)!它達成了!它用一種我們意想不到的、「隨機」的方式,同時實現了「排序陣列」的搜尋效率、和「鏈結串列」的新增彈性。

我們從兩個最「嚴謹守序」的結構(堆疊 LIFO 和佇列 FIFO)開始,理解了「限制」如何為電腦打造出「記憶」和「公平」。最後,我們又被「跳躍串列」的「隨機之美」所震撼。它告訴我們,有時候,要解決一個複雜的「秩序」問題,你需要的不是更「嚴格」的規則,而是一枚「硬幣」,以及擁抱「隨機」的勇氣。


留言
avatar-img
政大雜學筆記
3會員
49內容數
我在政大的一些紀錄
你可能也想看
Thumbnail
動一步要花錢,動兩步卻免費,這其中藏了什麼詐?聰明的你想到了嗎?
Thumbnail
動一步要花錢,動兩步卻免費,這其中藏了什麼詐?聰明的你想到了嗎?
Thumbnail
5 月將於臺北表演藝術中心映演的「2026 北藝嚴選」《海妲・蓋柏樂》,由臺灣劇團「晃晃跨幅町」製作,本文將以從舞台符號、聲音與表演調度切入,討論海妲・蓋柏樂在父權社會結構下的困境,並結合榮格心理學與馮.法蘭茲對「阿尼姆斯」與「永恆少年」原型的分析,理解女人何以走向精神性的操控、毀滅與死亡。
Thumbnail
5 月將於臺北表演藝術中心映演的「2026 北藝嚴選」《海妲・蓋柏樂》,由臺灣劇團「晃晃跨幅町」製作,本文將以從舞台符號、聲音與表演調度切入,討論海妲・蓋柏樂在父權社會結構下的困境,並結合榮格心理學與馮.法蘭茲對「阿尼姆斯」與「永恆少年」原型的分析,理解女人何以走向精神性的操控、毀滅與死亡。
Thumbnail
這道題你一定會解,但你知道怎麼把迴圈改寫成「迭代」嗎?這招學起來!
Thumbnail
這道題你一定會解,但你知道怎麼把迴圈改寫成「迭代」嗎?這招學起來!
Thumbnail
班上闖來了一個陌生人!該如何快狠準揪出他?這道經典考題的解法,遠比你想的還要多種 ......
Thumbnail
班上闖來了一個陌生人!該如何快狠準揪出他?這道經典考題的解法,遠比你想的還要多種 ......
Thumbnail
背景:從冷門配角到市場主線,算力與電力被重新定價   小P從2008進入股市,每一個時期的投資亮點都不同,記得2009蘋果手機剛上市,當時蘋果只要在媒體上提到哪一間供應鏈,隔天股價就有驚人的表現,當時光學鏡頭非常熱門,因為手機第一次搭上鏡頭可以拍照,也造就傳統相機廠的殞落,如今手機已經全面普及,題
Thumbnail
背景:從冷門配角到市場主線,算力與電力被重新定價   小P從2008進入股市,每一個時期的投資亮點都不同,記得2009蘋果手機剛上市,當時蘋果只要在媒體上提到哪一間供應鏈,隔天股價就有驚人的表現,當時光學鏡頭非常熱門,因為手機第一次搭上鏡頭可以拍照,也造就傳統相機廠的殞落,如今手機已經全面普及,題
Thumbnail
這是一場修復文化與重建精神的儀式,觀眾不需要完全看懂《遊林驚夢:巧遇Hagay》,但你能感受心與土地團聚的渴望,也不急著在此處釐清或定義什麼,但你的在場感受,就是一條線索,關於如何找著自己的路徑、自己的聲音。
Thumbnail
這是一場修復文化與重建精神的儀式,觀眾不需要完全看懂《遊林驚夢:巧遇Hagay》,但你能感受心與土地團聚的渴望,也不急著在此處釐清或定義什麼,但你的在場感受,就是一條線索,關於如何找著自己的路徑、自己的聲音。
Thumbnail
% 這個符號在 SQL 能做什麼,你都記得嗎?
Thumbnail
% 這個符號在 SQL 能做什麼,你都記得嗎?
Thumbnail
從範例學python的目標讀者: 針對剛進入的初學者,想學習Python語言。 有基礎本數學邏輯基礎即可。 從小遊戲學python的目標讀者: 針對已經有經驗的C/C++, Python, 或其他有程式基礎的讀者。 想實作一些小專案,從實做中學習如何分析需求、元件分拆、到底層實作
Thumbnail
從範例學python的目標讀者: 針對剛進入的初學者,想學習Python語言。 有基礎本數學邏輯基礎即可。 從小遊戲學python的目標讀者: 針對已經有經驗的C/C++, Python, 或其他有程式基礎的讀者。 想實作一些小專案,從實做中學習如何分析需求、元件分拆、到底層實作
Thumbnail
這篇文章說明演算法的定義、特徵以及一個有趣的小知識。演算法被定義為解決問題的流程,並以機車故障為例說明。文章也列出演算法的五個特徵:輸入、有限性、明確性、有效性及輸出。最後,文章提及世界上公認的第一個演算法是歐幾裡德演算法。
Thumbnail
這篇文章說明演算法的定義、特徵以及一個有趣的小知識。演算法被定義為解決問題的流程,並以機車故障為例說明。文章也列出演算法的五個特徵:輸入、有限性、明確性、有效性及輸出。最後,文章提及世界上公認的第一個演算法是歐幾裡德演算法。
Thumbnail
文字處理 101!把輸入內容做小寫轉換是很常見的應用唷~
Thumbnail
文字處理 101!把輸入內容做小寫轉換是很常見的應用唷~
Thumbnail
嚴格來說我也不算是科班出身,但對於作者所說的大多數優缺點都是認同的;至於成本的部份,因為美國跟台灣還是有所差異,從「教育成本」的角度來看,在台灣讀一個相關學位應該是相當值得才對。不管是在哪個領域或專業,每個人都應該對自己的「教育」或「學習」負最大責任,努力爭取機會,創造讓自己有「實務學習」的舞台。
Thumbnail
嚴格來說我也不算是科班出身,但對於作者所說的大多數優缺點都是認同的;至於成本的部份,因為美國跟台灣還是有所差異,從「教育成本」的角度來看,在台灣讀一個相關學位應該是相當值得才對。不管是在哪個領域或專業,每個人都應該對自己的「教育」或「學習」負最大責任,努力爭取機會,創造讓自己有「實務學習」的舞台。
Thumbnail
本文分析導演巴里・柯斯基(Barrie Kosky)如何運用極簡的舞臺配置,將布萊希特(Bertolt Brecht)的「疏離效果」轉化為視覺奇觀與黑色幽默,探討《三便士歌劇》在當代劇場中的新詮釋,並藉由舞臺、燈光、服裝、音樂等多方面,分析該作如何在保留批判核心的同時,觸及觀眾的觀看位置與人性幽微。
Thumbnail
本文分析導演巴里・柯斯基(Barrie Kosky)如何運用極簡的舞臺配置,將布萊希特(Bertolt Brecht)的「疏離效果」轉化為視覺奇觀與黑色幽默,探討《三便士歌劇》在當代劇場中的新詮釋,並藉由舞臺、燈光、服裝、音樂等多方面,分析該作如何在保留批判核心的同時,觸及觀眾的觀看位置與人性幽微。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News