Algorithm PUBLISHED UPDATED

認識演算法:常用資料結構

認識演算法:啟程 這篇文章中,我們提到可以幫助更好的設計演算法的其中一個策略是使用資料結構

我們也有提到資料結構是一種儲存和組織資料的方式,目的是為了讓資料的存取和修改更加方便。並且,資料結構有很多類型,為特定問題選擇合適的資料結構是演算法設計中的重要一環。

這篇文章我們要來更進一步的介紹常用資料結構。

以下是本篇文章的目錄:

  1. 陣列 (Array)
  2. 矩陣 (Matrix)
  3. 鏈結串列 (Linked List)
  4. 堆疊 (Stack)
  5. 佇列 (Queue)
  6. 有根樹 (Rooted Tree)
  7. 雜湊表 (Hash Table)

陣列(Array)

陣列是一種將資料元素(element)儲存在記憶體中一塊連續、不間斷空間的資料結構。

陣列的核心特性可以從它在記憶體中的儲存方式來理解,這直接決定了它的存取效率和限制。

陣列這個資料結構的存在,主要是為了利用電腦記憶體連續儲存的特性,來實現對資料集合最高效率的隨機存取。它的核心優勢如下:

但陣列還是有一些劣勢與限制,包含:

矩陣(Matrix)

矩陣(Matrix)是一種二維陣列,通常會透過一種或多種一維陣列來實現它在記憶體中的儲存。

矩陣的特性主要體現在它多樣的記憶體儲存方式上。我們有幾種將二維邏輯結構映射到一維線性記憶體中的策略,這些策略直接影響了資料的存取效率和使用彈性。

矩陣可以有效的組織和表示具有二維——或更高維——關係的數據集。凡是資料本身帶有「列」和「行」的邏輯概念,例如表格、棋盤、圖像像素等,使用矩陣來表示都是非常自然且直觀的。

鏈結串列(Linked List)

鏈結串列(Linked List)是一種資料結構,其中的物件透過各自內部的指標(pointer)來決定它的線性排列順序,而不是像陣列那樣由記憶體中的連續位置決定。

鏈結串列是一種極具彈性的動態集合表示方式,它的核心特性圍繞著「節點」與「指標」兩個概念。以下是關於鏈結串列特性的詳細介紹:

  1. 節點(Node)結構

    • 鏈結串列中的每一個元素都是一個獨立的物件,通常稱為「節點」。

    • 每個節點至少包含兩個屬性:一個是用於儲存資料的 key,以及一個或多個用於指向其他節點的指標(pointer)。節點也可以包含其他的附屬資料。

  2. 指標決定順序

    • 與陣列不同,鏈結串列的線性順序是由節點內的指標決定的。在一個雙向鏈結串列(doubly linked list) 中,每個節點 x 會有:

      • x.next:指向其後繼節點。

      • x.prev:指向其前驅節點。

    • 串列的第一個元素稱為「頭」(head),其 prev 指標為 NIL(空)。最後一個元素稱為「尾」(tail),其 next 指標為 NIL。整個串列的開頭由一個指向 head 的屬性 L.head 來標識。

  3. 多樣的形式

    • 鏈結串列可以根據結構和用途的不同,分為多種類型:

      • 單向(Singly Linked) vs. 雙向(Doubly Linked):單向串列的節點只有 next 指標,而雙向串列則同時有 nextprev 指標。

      • 排序(Sorted) vs. 未排序(Unsorted):排序串列中的元素會根據其 key 值的大小進行排列,最小值在頭部,最大值在尾部。未排序串列則沒有此限制。

      • 環狀(Circular) vs. 非環狀:環狀串列的尾部 next 指標會指向頭部,而頭部的 prev 指標會指向尾部,形成一個環。

  4. 哨兵(Sentinel)的應用

    • 為了簡化處理邊界條件——例如:在串列的頭部或尾部進行插入/刪除),可以引入一個稱為「哨兵」的虛設物件 L.nil

    • 這個哨兵位於頭部和尾部之間,使串列變成一個環狀結構。例如:哨兵的 next 指向真正的頭部,prev 指向真正的尾部。這樣一來,所有的插入和刪除操作都可以視為在兩個節點之間進行,無需再針對頭/尾的特殊情況撰寫額外程式碼,從而簡化了實作。

鏈結串列的主要目的是提供一個對於「動態集合」更具彈性的表示方式。在許多應用場景中,我們需要一個可以高效新增、刪除元素的資料集合,而傳統陣列在這些操作上有效能瓶頸。鏈結串列的線性順序是由指標決定的,而非固定的記憶體索引,這正是它靈活性的根源。而根據以上的說明,我們可以整理出兩大鏈結串列的優勢:

然而,鏈結串列也有它的劣勢存在:

鏈結串列的常見應用包括:

堆疊(Stack)

堆疊(Stack)是一種動態集合(dynamic set),它刪除的元素永遠是最近被插入的那一個,因此實踐了「後進先出」(Last-In, First-Out, LIFO) 的原則。以下是堆疊特性的詳細介紹:

堆疊這個資料結構的存在,是為了解決一類具有「後進先出」特性的問題。它的設計簡單而專一,從而帶來了顯著的優勢,但也伴隨著一些限制。以下整理出堆疊的優勢與劣勢:

管理副程式呼叫(Subroutine Linkage) 是堆疊最經典的應用之一。當我們的程式碼呼叫一個函式時,系統會將返回位址、參數和區域變數等資訊 PUSH 到一個稱為「呼叫堆疊」(Call Stack) 的記憶體區域。如果這個函式又呼叫了另一個函式,新的資訊會再次被 PUSH 到堆疊頂部。當函式執行完畢返回時,對應的資訊會從堆疊中 POP 出來,程式流程就能回到之前的位置。這種後進先出的機制完美地處理了函式的巢狀呼叫。

佇列(Queue)

佇列(Queue)是一種動態集合(dynamic set),它是一種遵循「先進先出」(First-In, First-Out, FIFO)原則的資料結構,就像現實生活中的排隊隊伍一樣,最先進入佇列的元素,將會是第一個被移除的元素。以下是佇列特性的詳細介紹:

佇列的存在是為了解決那些需要按順序處理任務或資料的場景。它的核心價值在於它內建的「公平性」和「順序性」。當多個項目需要共享有限的資源時(例如:一台印表機、一個 CPU 核心),佇列提供了一種簡單且可預測的管理機制,確保先到的請求會被先處理。它將現實世界中「排隊」這個公平的行為,抽象化為一種電腦科學中的資料結構。以下整理出佇列的優勢與劣勢:

優勢

劣勢

佇列的 FIFO 特性使它在電腦科學領域有著廣泛的應用,以下是一些常見的例子:

有根樹(Rooted Tree)

有根樹是一種透過節點物件與指標(pointers)來表示非線性階層關係的連結式資料結構。它的核心特性在於其節點的組織方式以及節點間的連結關係。以下我們先來介紹有根樹的基本特性:

現實世界中大量的關係本質上是非線性的。傳統的連結串列(Linked lists)非常適合表示序列、隊伍這類線性關係的資料。然而,當我們需要模擬或處理具有階層、分支或從屬關係的資料時,例如公司組織圖、資料夾結構、家族譜系等,線性結構就顯得力不從心。有根樹提供了一種直觀且高效的方式來模型化這些複雜的非線性階層關係。

以下我們更進一步的來介紹常見的有根樹類型:

二元樹(Binary Tree)

二元樹是一種每個節點最多擁有兩個子節點(一個左子節點和一個右子節點)的樹狀資料結構。這是最基礎的樹狀表示法之一。以下介紹二元樹的特性:

堆積(Heap)

堆積(Heap)是一種陣列物件,我們可以將它視為一個近似完整的二元樹(nearly complete binary tree)。

堆積是一個非常高效的資料結構,其核心特性圍繞著它的特殊結構與元素排序規則。以下來詳細介紹堆積的特性:

堆積這個資料結構的出現,主要是為了解決特定演算法的效率問題,並提供一種高效管理資訊的設計技巧。常見應用包含堆積排序(Heapsort)和優先佇列(Priority Queue)。

二元搜尋樹(Binary Search Trees)

二元搜尋樹是一種根植於二元樹的資料結構,它的組織方式滿足一個關鍵特性:對於樹中的任何節點 x,其左子樹中所有節點的值都小於或等於 x 的值,而其右子樹中所有節點的值都大於或等於 x 的值。

二元搜尋樹的核心特性都圍繞著它獨特的「二元搜尋樹屬性」(binary-search-tree property) 展開,這也決定了它的結構與行為。以下詳細介紹二元搜尋樹的特性:

紅黑樹(red-black trees)

紅黑樹是一種自平衡的二元搜尋樹,它透過為每個節點賦予「紅色」或「黑色」的顏色屬性,並遵循一套特定的規則,來確保樹中從根到葉子的最長路徑不會超過最短路徑的兩倍長,從而將樹的高度維持在對數級別 O(lgn)O(\lg n),進而保證了搜尋、插入和刪除等操作都能在最壞情況下維持高效能。

紅黑樹的核心在於它不僅僅是一棵二元搜尋樹,還必須始終滿足以下五個「紅黑特性」。正是這些特性的共同作用,才使得紅黑樹能夠自我維持平衡。

首先,我們需要理解紅黑樹的結構。除了像普通二元搜尋樹一樣擁有 key(鍵值)、left(左子節點)、right(右子節點)和 p(父節點)之外,每個節點還額外儲存一個 color 屬性,其值為「紅色」或「黑色」。

以下是紅黑樹必須遵守的五個特性:

  1. 每個節點不是紅色就是黑色。

    這是最基本的顏色規定,確保了每個節點都有一個明確的顏色狀態。

  2. 根節點(Root)必須是黑色。

    此規則確保了從根節點開始的路徑總是從一個黑色節點出發。

  3. 每個葉子節點(NIL)都是黑色。

    這裡的「葉子節點」是一個非常關鍵的概念。在紅黑樹的理論模型中,它指的不是沒有子節點的鍵值節點,而是指向的空(NIL)位置。為了方便程式碼實作,通常會用一個稱為「哨兵」(sentinel)的單一黑色節點 T.nil 來代表所有這些NIL葉子,以及根節點的父節點。

  4. 如果一個節點是紅色的,那麼它的兩個子節點都必須是黑色的。

    這個特性是維持平衡的關鍵之一。它直接禁止了任何路徑上出現兩個連續的紅色節點。這也意味著,從任何節點出發向下的路徑中,紅色節點的數量不會超過黑色節點。

  5. 對於每個節點,從該節點到其所有後代葉子節點的每條簡單路徑上,都包含相同數量的黑色節點。

    這個特性是紅黑樹最精妙也最強大的規則。它定義了一個概念叫做「黑高(black-height)」,指從某個節點 x 出發(不含 x 本身)到其後代葉子節點的任何路徑上的黑色節點數量,記為 bh(x)\operatorname { b h } ( x )。此規則確保了對於任何一個節點,其所有向下的路徑「黑色長度」都是相等的。結合第四個特性,這保證了沒有任何一條路徑會比其他路徑長太多,從而讓樹保持「近似平衡」。

標準的二元搜尋樹在理想情況下效能很好,但它有一個致命的缺點:它的效能完全取決於樹的高度 h,所有基本操作的時間複雜度都是 O(h)O(h)。如果插入的資料是隨機的,樹的高度期望值是 O(lgn)O(\lg n),效能優異。但如果插入的資料是已排序的(例如,依序插入 1, 2, 3, 4, 5),二元搜尋樹會退化成一個鏈結串列(linked list),樹高 h 會變成 nn,此時操作的效能會急遽下降到 O(n)O(n),完全喪失了樹狀結構的優勢。

紅黑樹的誕生就是為了解決這個「最壞情況」下的效能崩潰問題。它透過上述的五個特性,強制約束樹的結構,確保樹的高度始終維持在 O(lgn)O(\lg n) 的量級。如此一來,即使在最壞的情況下,紅黑樹的各種動態集合操作(如搜尋、插入、刪除、找最大/最小值等)的時間複雜度也能保證為 O(lgn)O(\lg n)

介紹完以上四種常見的有根樹類型,我們可以用以下簡單的比喻來更清楚的了解它們之間的關係:

首先,二元樹(Binary Tree) 是這個家族的始祖。而堆積(Heap)二元搜尋樹(Binary Search Tree) 是始祖的兩個孩子,它們繼承了始祖的特徵,但發展出完全不同的個性。最後,紅黑樹(Red-Black Tree) 則是二元搜尋樹這個孩子為了讓自己更強大、更可靠,而進化出的「專家」型態。

雜湊表(Hash Tables)

雜湊表是一種高效的資料結構,它透過一個稱為雜湊函數(hash function)的特殊函數,將一個鍵(key)直接計算並對應到陣列中的特定位置——稱為槽位(slot),以實現快速的資料存取。

要深入理解雜湊表,我們可以將它的特性拆解為以下幾個核心觀念:

雜湊表的出現主要是為了解決直接定址表(Direct-address Table) 的根本性限制。

雜湊表的優勢包含以下:

然而,雜湊表還是有它的劣勢:

參考資料

Introduction to Algorithms, fourth edition

附錄

本文為系列文章,目前寫了五篇:

  1. 認識演算法:啟程
  2. 認識演算法:常用資料結構
  3. 認識演算法:各種比較排序
  4. 認識演算法:各種線性時間排序
  5. 認識演算法:選擇問題