1. 程式人生 > 程式設計 >資料結構基礎知識: 表 棧 佇列 樹 雜湊 堆

資料結構基礎知識: 表 棧 佇列 樹 雜湊 堆

1. 表,棧和佇列

表,棧和佇列是電腦科學中最簡單和最基本的三種底層資料結構。事實上,每一個有意義的程式都將明晰地至少使用一種這樣的資料結構,而棧則在程式中總是要間接地用到,不管你在程式中是否做了宣告。

1.1 抽象資料型別(ADT)

在計算機軟體程式設計中,我們會接觸到諸如整型,浮點型,字元型,布林型等基本資料型別,也有一些更為複雜的複合資料型別,如陣列,字典(散列表),元組等。如果我們拋開這些資料型別具體實現,進一步抽象,給出更一般的定義,即每一種資料型別實際是一些特定操作的集合。我們稱這些操作的集合抽象資料型別abstract data type,ADT)。ADT 是數學意義上的抽象,它不約束各個操作的具體實現,對於每種 ADT 並不存在什麼法則來告訴我們必須要有哪些操作,這只是一個設計決策。

1.2 表ADT

表是一種形如 A_1,A_2,A_3,\ldots,A_N 的資料結構。我們說這個表的大小是 N 。我們稱大小為0的表為空表(empty list)。對於除空表以外的任何表,我們說A_{i+1}(i<N)後繼A_i(或繼A_i之後)並稱A_{i-1}(i>1)前驅A_i。定義表ADT的操作集合通常包含:

  • PrintList: 打印表
  • MakeEmpty: 返回空表
  • Find: 返回關鍵字(key)首次出現的位置
  • Insert: 從表的指定位置插入關鍵字(key)
  • Delelte: 從表的指定位置刪除關鍵字(key)
  • FindKth: 返回指定位置上的元素

1.2.1 簡單陣列實現

我們最容易想到實現表的方法就是陣列。使用陣列實現表的各項操作,顯而易見的時間複雜度是:

  • PrintList: O(N)
  • Find: O(N)
  • FindKth: O(1)
  • Insert: O(N)
  • Delete: O(N)

我們不難發現,當插入和刪除N個元素時,需要花費O(N^2)的時間,執行慢且表的大小必須事先已知。因此當需要具有插入和刪除操作時,通常不使用簡單陣列來實現。

1.2.2 連結串列實現

為了避免插入和刪除的線性開銷,我們需要允許表可以不連續儲存,否則表的部分或全部需要整體移動。因此,這種情況下更好的實現方式是連結串列(linked list)。

連結串列由一系列不必在記憶體中相連的結構組成。每一個結構均含有表元素和指向包含該元素後繼元的結構的指標。我們稱之為Next指標。最後一個單元的Next指標指向Null。連結串列分為:單連結串列,雙連結串列,迴圈連結串列。

連結串列的 C 語言實現:

#include <stdlib.h>

struct Node
{
    int Element;
    Node* Next;
};

int
IsEmpty(Node* L)
{
    return L->Next == NULL;
}

int
IsLast(Node* P,Node* L)
{
    return P->Next == NULL;
}

Node*
Find(int x,Node* L)
{
    Node* P;
    P = L->Next;
    while(P != NULL && P->Element != x)
        P = P->Next;
    return P;
}

Node*
FindPrevious(int x,Node* L)
{
    Node* P;
    
    P = L;
    while(P->Next != NULL && P->Next->Element != x)
        P = P->Next;
    
    return P;
}

void
Delete(int x,Node* L)
{
    Node* P;
    Node* TmpCell;
    
    P = FindPrevious(x,L);
    
    if (!IsLast(P,L)) {
        TmpCell = P->Next;
        P->Next = TmpCell->Next;
        free(TmpCell);
    }
}

void
Insert(int x,Node* L,Node* P)
{
    Node* TmpCell;
    
    TmpCell = (Node*)malloc(sizeof(struct Node));
    
    if (TmpCell != NULL)
        printf("Out of space!!!");
    
    TmpCell->Element = x;
    TmpCell->Next = P->Next;
    P->Next = TmpCell;
}

void
DeleteList(Node* L)
{
    Node* P;
    Node* Tmp;
    P = L->Next;
    L->Next = NULL;
    while(P != NULL)
    {
        Tmp = P->Next;
        free(P);
        P = Tmp;
    }
}
複製程式碼

1.2 棧ADT

棧(stack)是限制插入和刪除只能在一個位置上進行的表,該位置是表的末端,叫做棧的頂(top)。對棧的基本操作有Push(進棧)和Pop(出棧),前者相當於插入,後者則是刪除最後插入的元素。最後插入的元素可以通過使用Top操作在執行Pop之前讀取。

由於棧是一個表,因此任何實現表的方法都能實現棧。通常使用陣列是一個較為簡便的方法。

1.3 佇列ADT

和棧一樣,佇列(queue)也是表。不同的是,使用佇列時插入在一端進行而刪除則在另一端進行。

佇列的基本操作是Enqueue (入隊),它是在表的末端(rear 隊尾)插入一個元素,還有 Dequeue (出隊),它是刪除(或返回)在表的開頭(front 對頭)的元素。

2. 樹

2.1 基礎知識

對於大量的輸入資料,連結串列的線性訪問時間太慢,不宜使用。而“樹”大部分操作的執行時間平均為O(log N)

一課樹是N個節點N-1條邊的集合,其中的一個節點叫做根。

路徑

從節點n_1n_k的路徑(path)定義為節點 n_1,n_2,...,n_k 的一個序列,使得對於 1 \le i < k,節點 n_in_{i+1} 的父親。這個路徑的長(length)為該路徑上的邊的條數,即 k-1 。從每一個節點到它自己都有一條長為0的路徑。從根到每個節點有且僅有一條路徑。

深度

對於任意節點n_in_i的深度(depth)為從根到n_i的唯一路徑的長。因此,根的深度為0

高度

n_i的高(height)是從n_i到一片樹葉的最長路徑的長。因此,所有樹葉的高度都是0

祖先(ancestor)和後裔(descendant)

如果存在從n_1n_2的一條路徑,那麼n_1n_2的一位祖先而n_2n_1的一個後裔。如果n_1 \ne n_2,那麼n_1n_2的一位真祖先(proper ancestor)而n_2n_1的一個真後裔(proper descendant)。

2.2 樹的實現

將每個節點的所有兒子都放在樹節點的連結串列中。FirstChild 是指向第一個兒子的指標,NextSibling 指向下一個兄弟節點。

typedef struct TreeNode *PtrToNode;

struct TreeNode
{
    ElementType Element;
    PtrToNode FirstChild;
    PtrToNode NextSibling;
}
複製程式碼

2.3 樹的遍歷

先序遍歷(preorder traversal)

在先序遍歷中,對節點的處理工作是在它的諸兒子節點被處理之前進行的。例如:列印目錄樹形結構圖,先列印父節點,再遞迴列印子節點。

後序遍歷(postorder traversal)

在後序遍歷中,在一個節點處的工作是在它的諸兒子節點被計算後進行的。例如:計算目錄所佔磁碟空間,在得到父節點佔用空間前,需要先遞迴計運算元節點所佔用的磁碟空間,最後才能逐級向上得到根節點的磁碟總佔用空間。

中序遍歷(inorder traversal)

用於二叉樹。遍歷順序:左子樹,節點,右子樹。

2.4 二叉樹(binary tree)

在二叉樹中,每個節點最多隻有兩個兒子。

二叉樹的平均深度為O(\sqrt{N}),而對於特殊型別的二叉樹,如二叉查詢樹(binary search tree),其平均深度是O(log N)

2.4.1 二叉樹實現

因為一棵二叉樹最多有兩個兒子,所以我們可以用指標直接指向它們。樹節點的宣告在結構上類似於雙連結串列的宣告。在宣告中,一個節點就是由Key資訊加上兩個指向其他節點的指標(Left 和  Right)組成的結構。

typedef struct TreeNode *PtrToNode;
typedef struct PtrToNode Tree;

struct TreeNode
{
    ElementType Element;
    Tree Left;
    Tree Right;
}
複製程式碼

二叉樹有許多與搜尋無關的重要應用。二叉樹的主要用處之一是在編譯器的設計領域。如二元表示式樹。

2.4.2 查詢樹ADT——二叉查詢樹

二叉樹的一個重要的應用是它們在查詢中的使用。使二叉樹成為二叉查詢樹的性質是,對於樹中的每個節點X,它的左子樹所有關鍵字的值小於X,而它右子樹中所有關鍵字值大於X的關鍵字值。

操作集合:

  • MakeEmpty
  • Find
  • FindMin 和 FindMax
  • Insert
  • Delete

2.4.3 AVL 樹

AVL(Adelson-Velskii 和 Landis)樹是帶有平衡條件的二叉查詢樹。這個平衡條件必須要容易保持,而且它必須保證樹的深度是O(log N)。最簡單的想法是要求左右子樹具有相同的高度。另一種平衡條件是要求每個節點都必須要有相同高度的左子樹和右子樹。雖然這種平衡條件保證了樹的深度小,但是它太嚴格,難以使用,需要放寬條件。

一棵AVL樹是其每個節點的左子樹和右子樹的高度最多差1的二叉查詢樹。

單旋轉

雙旋轉

2.4.4 伸展樹(splay tree)

伸展樹保證從空樹開始任意連續M次對樹的操作最多花費O(M log N)的時間。

2.5 B-樹

雖然迄今為止我們所看到的查詢樹都是二叉樹,但是還有一種常用的查詢樹不是二叉樹。這種樹叫做B-樹(B-tree)。

階為M的B-樹是一棵具有下列結構特性的樹:

  • 樹的根或者是一片樹葉,或者其兒子數在2和M之間。
  • 除根外,所有非樹葉節點的兒子數在[M/2]和M之間。
  • 所有的樹葉都在相同的深度上。

B-樹實際用於資料庫系統,在那裡樹被儲存在物理的磁碟上而不是主存中。一般來說,對磁碟的訪問要比任何的主存操作慢幾個數量級。如果我們使用M階B-樹,那麼磁碟訪問的次數是O(log_{M}N)

3. 雜湊

散列表的實現常常叫做雜湊(hashing)。雜湊是一種用於以常數平均時間執行插入,刪除和查詢的技術。但是,那些需要元素間任何排序資訊的操作將不會得到有效的支援。因此,諸如 FindMin,FindMax 以及以線性時間按排序順序將整個表進行列印的操作都是雜湊所不支援的。

3.1 一般想法

理想的散列表資料結構只不過是一個包含關鍵字(key)的具有固定大小的陣列。典型情況下,一個關鍵字就是一個帶有相關值(例如工資資訊)的字串。我們把表的大小記作Table-Size,並將其理解為雜湊資料結構的一部分而不僅僅是浮動於全域性的某個變數。通常的習慣是讓表從0Table-Size - 1變化。

每個關鍵字被對映到從0Table-Size - 1這個範圍中的某個數,並且被放到適當的單元中。這個對映就叫做雜湊函式hash function)。理想情況下它應該運算簡單並且應該保證任何兩個不同的關鍵字對映到不同的單元。不過,這是不可能的,因為單元的數目是有限的,而關鍵字實際上是用不完的。因此,我們尋找一個雜湊函式,該函式要在單元之間均勻地分配關鍵字。這就是雜湊的基本想法。剩下的問題則是選擇一個函式,決定當兩個關鍵字雜湊到同一個值的時候(稱為衝突collision)應該做什麼以及如何確定散列表的大小。

3.2 雜湊函式

3.2.1 輸入整數關鍵字

如果輸入的關鍵字是整數,則一般合理的方法就是直接返回“Key mod TableSize”(關鍵字對錶大小取模)的結果,除非Key碰巧具有某些不理想的性質。例如,若表的大小是10,而關鍵字都以0為個位,這意味所有關鍵字取模運算的結果都是0(都能被10整除)。這種情況,好的辦法通常是保證表的大小是素數(也叫質數,只能被1和自身整除)。當輸入的關鍵字是隨機的整數時,雜湊函式不僅算起來簡單而且關鍵字的分配也很均勻

3.2.2 輸入字串關鍵字

通常,關鍵字是字串;在這種情形下,雜湊函式需要仔細地選擇。

一種方法是把字串中字元的ASCII碼值加起來。以下該方法的C語言實現。

typedef unsigned int Index;

Index
Hash(const char *Key,int TableSize)
{
    unsigned int HashVal = 0;
    while(*Key != '\0')
        HashVal += *Key++;
    return HashVal % TableSize;
}
複製程式碼

這種方法實現起來簡單而且能很快地計算出答案。不過,如果表很大,則函式將不會很好地分配關鍵字。例如,設TableSize = 10007(10007是素數),並設所有的關鍵字至多8個字元長。由於char型量的值最多是127,因此雜湊函式只能取在0和1016之間,其中1016=127\times8。這顯然不是一種均勻分配。

第二種方法。假設Key至少有兩個字元外加NULL結束符。值27表示英文字母表的字母個數外加一個空格,而729=27^2。該函式只考查前三個字元,假如它們是隨機的,而表的大小像前面那樣還是10007,那麼我們就會得到一個合理的均衡分配。

Index
Hash(const char *Key,int TableSize)
{
    return (Key[0] + 27*Key[1] + 729*Key[2]) % TableSize;
}
複製程式碼

可是不巧的是,英文並不是隨機的。雖然3個字元(忽略空格)有26^3 = 17576種可能組合,但查驗詞彙量足夠大的聯機詞典卻揭示:3個字母的不同組合數實際只有2851。即使這些組合沒有衝突,也不過只有表的28%被真正雜湊到。因此,雖然很容易計算,但是當散列表足夠大的時候這個函式還是不合適的。

一個更好的雜湊函式。這個雜湊函式涉及到關鍵字中的所有字元,並且一般可以分佈得很好。計算公式如下:

\sum_{i=0}^{KeySize-1} Key[KeySize-1-i]\cdot32^i

它根據Horner法則計算一個(32的)多項式。例如,計算h_k=k_1 + 27k_2 + 27^2k_3的另一種方式是藉助於公式h_k=(k_3 \times 27 + k_2)\times 27 + k_1進行。Horner法則將其擴充套件到用於n次多項式。

Index
Hash(const char *Key,int TableSize)
{
    unsigned int HashVal = 0;

    while(*Key != '\0')                     /* 1 */
        HashVal = (HashVal << 5) + *Key++;  /* 2 */
    return HashVal % TableSize;             /* 3 */
}
複製程式碼

這裡之所以用 32 替代27,是因為用32作乘法不是真的去乘,而是移動二進位制的5位。為了運算更快,程式第2行的加法可以用按位異或來代替。雖然就表的分佈而言未必是最好的,但確實具有及其簡單的優點。如果關鍵字特別長,那麼該雜湊函式計算起來將會花費過多的時間,不僅如此,前面的字元還會左移出最終的結果。這種情況,通常的做法是不使用所有字元。此時關鍵字的長度和性質將影響選擇。

3.3 衝突解決

解決了關鍵字均勻對映的問題,剩下的主要程式設計細節是解決衝突的消除問題。如果當一個元素被插入時另一個元素已經存在(雜湊值相同),那麼就產生了衝突,這種衝突需要消除。解決這種衝突的方法有幾種。最簡單的兩種是:分離連結法和開放定址法。

3.3.1 分離連結法(separate chaining)

分離連結法是將雜湊到同一個值的所有元素保留到一個表中。為了方便起見,這些表都有表頭,實現方法與表ADT相同。如果空間很緊,則更可取的方法是避免使用這些表頭。

型別宣告:

#ifndef _HashSep_H

struct ListNode;
typedef struct ListNode *Position;
struct HashTbl;
typedef struct HashTbl *HashTable;

HashTable InitializeTable(int TableSize);
void DestroyTable(HashTable H);

ElementType Retrieve(Position P);

#endif

struct ListNode
{
    ElementType Element;
    Position Next;
};

typedef Position List;

struct HashTbl
{
    int TableSize;
    List *TheLists;
};
複製程式碼

初始化函式:

HashTable
InitializeTable(int TableSize)
{
    HashTable H;
    int i;

    if (TableSize < MinTableSize)
    {
        Error("Table size too small");
        return NULL;
    }

    H = malloc(sizeof(struct HashTbl));
    if (H == NULL)
        FatalError("out of space!!!");

    H->TableSize = NextPrime(TableSize);                /* 1 設定素數大小 */
    H->TheLists = malloc(sizeof(List) * H->TableSize);  /* 2 */
    if (H->TheLists == NULL)
        FatalError("Out of space!!!");
    
    /**
      * 分配連結串列表頭
      * 
      * 給每一個表設定一個表頭,並將 Next 指向 NULL。如果不用表頭,以下程式碼可省略。
      */
    for(i = 0; i < H->TableSize; i++)
    {
        H->TheLists[i] = malloc(sizeof(struct ListNode)); /* 3 */
        if (H->TheLists[i] == NULL)
            FatalError("Out of space!!!");
        else
            H->TheLists[i]->Next = NULL;
    }

    return H;
}
複製程式碼

以上程式低效之處是標記為3處malloc執行了H->TableSize次。這可以通過迴圈開始之前呼叫一次malloc操作:

H->TheLists = malloc(sizeof(struct ListNode) * H->TableSize);
複製程式碼

Find 操作:

Position
Find(ElementType Key,HashTable H)
{
    Position P;
    List L;

    L = H->TheLists[ Hash(Key,H->TableSize) ];
    P = L->Next;
    while(P != NULL && P->Element != Key)   /* ElementType 為 int時比較。字串比較使用 `strcmp` */
        P = P->Next;

    return P;
}
複製程式碼

注意,判斷 P->Element != Key,這裡適用於整數。字串比較用 strcmp 替換。

Insert 操作:

void
Insert(ElementType Key,HashTable H)
{
    Position Pos,NewCell;
    List L;

    Pos = Find(Key,H);
    if (Pos == NULL)
    {
        NewCell = malloc(sizeof(struct ListNode));
        if(NewCell == NULL)
            FatalError("Out of space!!!");
        else
        {
            L = H->TheLists[ Hash(Key,H->TableSize) ];
            NewCell->Next = L->Next;
            NewCell->Element = Key;
            L->Next = NewCell;
        }
    }
}
複製程式碼

如果在雜湊中諸例程中不包括刪除操作,那麼最好不要使用表頭。因為這不僅不能簡化問題而且還要浪費大量的空間。

3.3.2 開放定址法(Open addressing hashing)

型別宣告:

#ifndef _HashQuad_H

typedef unsigned int Index;
typedef Index Position;

struct HashTbl;
typedef struct HashTbl *HashTable;

HashTable InitializeTable(int TableSize);
void DestroyTable(HashTable H);
Position Find(ElementType Key,HashTable H);
void Insert(ElementType Key,HashTable H);
ElementType Retrieve(Position P,HashTable H);
HashTable Rehash(HashTable H);

#endif

enum KindOfEntry { Legitimate,Empty,Deleted };

struct HashEntry
{
    ElementType Element;
    enum KindOfEntry Info;
};

typedef struct HashEntry Cell;

struct HashTbl
{
    int TableSize;
    Cell *TheCells;
};
複製程式碼

初始化開放定址散列表:

HashTable
InitializeTable(int TableSize)
{
    HashTable H;
    int i;

    if (TableSize < MinTableSize)
    {
        Error("Table size too small");
        return NULL;
    }

    /* 給散列表分配記憶體 */
    H = malloc(sizeof(struct HashTbl));
    if (H == NULL)
        FatalError(Out of space!!!);
    
    H->TableSize = NextPrime(TableSize);    /* 大於 TableSize 的第一個素數 */

    /* 給陣列所有單元分配記憶體 */
    H->TheCells = malloc(sizeof(Cell) * H->TableSize);
    if(H->TheCells == NULL)
        FatalError("Out of space!!!");
    
    for (i = 0; i < H->TableSize; i++)
        H->TheCells[i].Info = Empty;

    return H;
}
複製程式碼

Find 操作:

Position
Find(ElementType Key,HashTbl H)
{
    Position CurrentPos;
    int CollisionNum;

    CollisionNum = 0;
    CurrentPos = Hash(Key,H->TableSize);
    while(H->TheCells[CurrentPos].Info != Empty &&
          H->TheCells[CurrentPos].Element != Key)
          /* 這裡可能需要使用 strcmp 字串比較函式 !! */
    {
        CurrentPos += 2 * ++CollisionNum - 1;
        if (CurrentPos >= H->TableSize)
            CurrentPos -= H->TableSize;
    }

    return CurrentPos;
}
複製程式碼

Insert 操作:

void Insert(ElementType Key,HashTable H)
{
    Position Pos;
    Pos = Find(Keu,H);
    if (H->TheCells[Pos].Info != Legitimate)
    {
        H->TheCells[Pos].Info = Legitimate;
        H->TheCells[Pos].Element = Key; /* 字串型別需要使用 strcpy 函式 */
    }
}
複製程式碼

3.4 散列表的應用

雜湊有著豐富的應用。編譯器使用散列表跟蹤原始碼中宣告的變數。這種資料結構叫做符號表(symbol table)。散列表是這種問題的理想應用,因為只有InsertFind操作。識別符號一般都不長,因此其雜湊函式能夠迅速被算出。

散列表常見的用途也出現在為遊戲編寫的程式中。當程式搜尋遊戲的不同的行時,它跟蹤通過計算機基於位置的雜湊函式而看到的一些位置。如果同樣的位置再出現,程式通常通過簡單移動變換來避免昂貴的重複計算。遊戲程式的這種一般特點叫做變換表(transposition table)

另外一個用途是線上拼寫檢驗程式。如果錯拼檢測(與糾正錯誤相比)更重要,那麼整個詞典可以被預先雜湊,單詞則可以在常數時間內被檢測。散列表很適合這項工作,因為以字母順序排列單詞並不重要;而以它們在檔案中出現的順序顯示出錯誤拼寫當然是可以接受的。

4. 優先佇列(堆)

4.1 為什麼需要優先佇列?

佇列是一種先進先出的表ADT,正常來說,先入隊的元素,會先出隊,意味沒有那個元素是特殊的,擁有“插隊”的優先權。這種平等,並不試用所有場景。有時,我們希望佇列中某類元素擁有比其他元素更高的優先順序,以便能提前得到處理。因此,我們需要有一種新的佇列來滿足這樣的應用,這樣的佇列叫做“優先佇列(priority queue)”。

4.2 優先佇列模型

優先佇列允許至少兩種操作:Insert(插入) ,以及 DeleteMin(刪除最小者)。Insert 操作等價於 Enqueue(入隊),而 DeleteMin 則是佇列中 Dequeue(出隊) 在優先佇列中的等價操作。DeleteMin 函式也變更它的輸入。

4.3 簡單實現

4.3.1 單連結串列實現

在表頭以O(1)執行插入操作,並遍歷該連結串列以刪除最小元,這又需要O(N)時間。另一種做法是,始終讓表保持排序狀態;這使得插入代價高昂(O(N))而DeleteMin花費低廉(O(1))。基於DeleteMin的操作次數從不多於插入操作次數的事實,前者也許是更好的辦法。

4.3.2 二叉查詢樹實現

使用二叉查詢樹,Insert 和 DeleteMin 這兩種操作的平均執行時間都是O(log N)

4.4 二叉堆(binary heap)

實現優先佇列更加普遍的方法是二叉堆,以至於當(heap)這個詞不加修飾地使用時一般都是指該資料結構(優先佇列)的這種實現。因此,我們單獨說堆時,就是指二叉堆。同二叉查詢樹一樣,堆也有兩個性質,即結構性堆序性。正如AVL樹一樣,對堆的一次操作可能破壞這兩個性質的一個,因此,堆的操作必須要到堆的所有性質都被滿足時才能終止。事實上這並不難做到。

4.4.1 結構性質

堆是一棵被完全填滿的二叉樹,有可能的例外是在底層,底層上的元素從左到右填入。這樣的樹稱為完全二叉樹(complete binary tree)。因為完全二叉樹很有規律,所以它可以用一個陣列表示而不需要指標。對於陣列任意位置i上的元素,其左兒子在位置2i上,右兒子在左兒子後的單元2i+1上,它的父親則在位置\lfloor i/2 \rfloor上。因此,不僅指標這裡不需要,而且遍歷該樹所需要的操作也極簡單,在大部分計算機上執行很可能非常快。

一個堆資料結構由一個陣列,一個代表最大值的整數以及當前的堆大小組成。

優先佇列宣告:

#ifndef _BinHeap_H

struct HeapStruct;
typedef struct HeapStruct *PriorityQueue;

PriorityQueue Initialize(int MaxElements);
void Destroy(PriorityQueue H);
void MakeEmpty(PriorityQueue H);
void Insert(ElementType X,PriorityQueue H);
ElementType DeleteMin(PriorityQueue H);
ElementType FindMin(PriorityQueue H);
int IsEmpty(PriorityQueue H);
int IsFull(PriorityQueue H);

#endif

struct HeapStruct
{
    int Capacity;
    int Size;
    ElementType *Elements;
}
複製程式碼

4.4.2 堆序性質

使操作被快速執行的性質是堆序(heap order)性。由於我們想要快速地找出最小元,因此,最小元應該在根上。如果我們考慮任意子樹也應該是一個堆,那麼任意節點就應該小於它的所有後裔。

PriorityQueue
Initialize(int MaxElements)
{
    PriorityQueue H;

    if (MaxElements < MinPQSize)
        Error("Priority queue size is too small");
    
    H = malloc(sizeof(struct HeapStruct));
    if (H == NULL)
        FatalError("Out of space!!!");
    
    H->Elements = malloc((MaxElements + 1)
                         * sizeof(ElementType));
    
    if (H->Elements == NULL)
        FatalError("Out of space!!!");

    H->Capacity =  MaxElements;
    H->Size = 0;
    H->Elements[0] = MinData;

    return H;
}
複製程式碼

根據堆序性質,最小元總是可以在根處找到。因此,我們以常數時間完成附加運算FinMin。

4.5 堆的基本操作

4.5.1 Insert (插入)

void
Insert(ElementType X,PriorityQueue H)
{
    int i;

    if (IsFull(H))
    {
        Error("Priority queue is full");
        return;
    }
    for (i = ++H->Size; H->Elements[i / 2] > X; i /= 2)
        H->Elements[i] = H->Elements[i / 2];
    H->Elements[i] = X;
}
複製程式碼

4.5.2 DeleteMin (刪除最小元)

ElementType
DeleteMin(PriorityQueue H)
{
    int i,Child;
    ElementType MinElement,LastElement;

    if (IsEmpty(H))
    {
        Error("Priority queue is empty");
        return H->Elements[0];
    }
    MinElement = H->Elements[1];
    LastElement = H->Elements[H->Size--];

    for(i = 1; i * 2 <= H->Size; i = Child)
    {
        Child = i * 2;
        if (Child != H->size && H->Elements[Child + 1]
                              < H->Elements[Child])
            Child++;

        if (LastElement > H->Elements[ Child ])
            H->Elements[i] = H->Elements[Child];
        else
            break;
    }
    H->Elements[i] = LastElement;
    return MinElement;
}
複製程式碼

4.6 優先佇列的應用

  • 作業系統設計:程式排程
  • 圖論演演算法
  • 選擇問題:從N個元素中找出第k個最大的元素
  • 事件模擬