資料結構與演算法分析 (優先佇列)

myxs發表於2017-04-27

標籤: Data-Structures


摘要:本節包括二叉堆,左式堆,斜堆和二項佇列幾種資料結構

1. 二叉堆

優先佇列至少支援下列2種操作:Insert,DeleteMIn(DeleteMax)。 優先佇列的實現:

  1. 連結串列
  2. 二叉查詢樹
  3. 二叉堆的陣列實現 堆是完全二叉樹,它的堆序性

優先佇列的宣告:

#ifndef BINHEAP_BINHEAP_H
#define BINHEAP_BINHEAP_H

struct HeapStruct;
typedef struct HeapStruct *PriorityQueue;

PriorityQueue Init(int MaxElements);
void Destroy(PriorityQueue H);
void MakeEmpty(PriorityQueue H);
void Insert(int X, PriorityQueue H);
int DeleteMin(PriorityQueue H);
int FindMin(PriorityQueue H);
int IsEmpty(PriorityQueue H);
int IsFull(PriorityQueue H);
#endif //BINHEAP_BINHEAP_H
struct HeapStruct{
    int Capactiy;
    int Size;
    int *Elems;
};

初始化以及銷燬:

PriorityQueue Init(int MaxElements){
    PriorityQueue H;
    H = malloc(sizeof(struct HeapStruct));
    H->Elems = malloc(sizeof(int)* (MaxElements + 1));
    H->Size = 0;
    H->Capactiy = MaxElements;
    H->Elems[0] = -9999;
    return H;
}

void Destroy(PriorityQueue H){
    free(H->Elems);
    free(H);
}

插入:

陣列大小加1後。在從最後一個元素的父節點回溯到根節點的過程中,比較節點和要插入元素X的大小,如果X小,則將父節點的值轉移到子節點中,直到遇到X比父節點值大的時候才退出迴圈,將X直接放入當前節點中。

void Insert(int X, PriorityQueue H){
    int i;
    if (IsFull(H)){
        return;
    }
    for(i = ++H->Size;H->Elems[i/2] > X; i /=2){
        H->Elems[i] = H->Elems[i/2];
    }
    H->Elems[i] = X;
    H->Elems[0] = H->Elems[1];
}

刪除最小元:

最小堆的最小元是根,直接刪除根。比較根的較小子節點和最後一個元素的大小,如果根的子節點小,則移動到父節點中,否則退出迴圈。

int DeleteMin(PriorityQueue H){
    int i,child;
    if (IsEmpty(H))
        return H->Elems[0];
    int X = H->Elems[H->Size--];
    int Min = H->Elems[1];

    for(i = 1; i * 2 <= H->Size; i = child){
        child = i * 2;
        if (child != H->Size && H->Elems[child + 1] < H->Elems[child]){
            child ++;
        }
        if (H->Elems[child] < X)
            H->Elems[i] = H->Elems[child];
        else
            break;
    }
    H->Elems[i] = X;
    return Min;
}

構建二叉堆-BUildHeap

以O(N)時間複雜度構建一個最小堆。

PriorityQueue BuildHeap(int *A, int N){
    PriorityQueue H;
    H = malloc(sizeof(struct HeapStruct));
    H->Size = N;
    H->Capactiy = N;
    H->Elems = malloc(sizeof(int) * (N + 1));
    int i;
    for( i = N/2; i > 0; i--){
        PercolateDown(i,A,N);
    }
    for (int j = 1; j <= N; ++j) {
        H->Elems[j] = A[j];
    }
    return H;
}

一個關鍵步驟即節點下濾(PercolateDewn) 主要是調整當前下標為index的節點的子樹的堆序性。第一步是找到較小子節點,第二步比較父節點和子節點的大小,如果不滿足堆序性,交換,重複直到滿足堆序性

void PercolateDown(int index, int *A,int N){
    int child;
    int t;
    for( int i = index; i*2<=N; i = child){
        child = i * 2;
        if (child != N && A[child] > A[child + 1])
            child++;
        if(A[child] < A[i]){
            t = A[i];
            A[i] = A[child];
            A[child] = t;
        }
    }
}

2. 左式堆

左式堆建立在二叉樹的基礎上,不同於二叉堆, 左式堆不是完全二叉樹,而且極不平衡。

性質: 零路徑長(NPL):從X到一個沒有左右兒子的節點的最短路徑的長

  1. 任一節點的NPL比所有兒子節點的NPL的最小值多1
  2. 最小堆的特性:父節點屬性值小於子節點屬性值
  3. 堆中任一節點的左兒子的NPL >= 右兒子的NPL

左式堆節點結構

struct TreeNode{
    int Elem;
    PriorityQueue Left;
    PriorityQueue Right;
    int Npl;
};

左式堆的基本操作是合併,O(logN)

  1. 如果有一個堆是空樹,直接返回另一個堆,否則根節點值大的堆和根節點值小的右子樹遞迴合併,形成新的左式堆
  2. 合併後的新堆作為較小堆的右子樹
  3. 如果堆根節點的NPL不符合左式堆的特性,交換左右子樹
  4. 更新根節點的NPL

    PriorityQueue Merge(PriorityQueue H1, PriorityQueue H2){
        if (H1 == NULL)
            return H2;
        if (H2 == NULL)
            return H1;
        if (H1->Elem < H2->Elem)
            return Merge1(H1,H2);
        else
            return Merge1(H2,H1);
    }
    
    
    PriorityQueue Merge1(PriorityQueue H1, PriorityQueue H2){
        if (H1->Left == NULL)// H1是單節點
            H1->Left = H2;
        else{
            H1->Right = Merge(H1->Right,H2);
            if (H1->Left->Npl < H1->Right->Npl)
                SwapChildren(H1);
            H1->Npl = H1->Right->Npl + 1;
        }
        return H1;
    }
    

插入操作可以看成單節點堆和另一個堆合併的情況,O(logN)

PriorityQueue Insert(int X, PriorityQueue H){
    Node P;
    P = malloc(sizeof(struct TreeNode));
    P->Elem = X;
    P->Left = P->Right = NULL;
    P->Npl = 0;
    H = Merge(P,H);
    return H;
}

刪除最小數操作直接刪除根節點,將左右子樹合併形成新的左式堆,O(logN)

當全部刪除後,會以從小到大的順序排列元素,可以作為一種排序方法

PriorityQueue DeleteMin(PriorityQueue H){
    PriorityQueue LeftHeap,RightHeap;
    if (IsEmpty(H))
        return NULL;
    else{
        LeftHeap = H->Left;
        RightHeap = H->Right;
        free(H);
        return Merge(LeftHeap,RightHeap);
    }
}

其它操作(上濾,增加或降低關鍵字值)

void PercolateUp(int index, int *A){
    int parent;
    for (int i = index; i / 2>0 ; i /=2) {
        parent = i/2;
        if (A[parent] > A[index]){
            int t = A[parent];
            A[parent] = A[index];
            A[index] = t;
        }
        else
            break;
    }
}

void DecreaseKey(int index, int Incre, int *A){
    A[index] -=Incre;
    PercolateUp(index,A);
}
void IncreaseKey(int index, int Incre, int *A,int N){
    A[index] +=Incre;
    PercolateDown(index,A,N);
}

3. 斜堆

斜堆和左式堆類似於伸展樹和AVL樹,後者都需要在節點中儲存額外的資訊如高度和零路徑

斜堆基本操作也是合併

  1. 如果一個是空堆,直接返回一個另一個斜堆
  2. 兩個斜堆都非空,那麼比較兩個根節點,取較小堆的根節點作為新的根節點,較小堆根節點的右子樹與較大堆遞迴合併
  3. 交換根的左右子樹

與左式堆的合併稍有不同的是第3步

  1. 如果堆根節點的NPL不符合左式堆的特性,交換左右子樹
  2. 更新根節點的NPL

4. 二項佇列

二項佇列是二項樹的集合。二項樹Bk由一個帶有兒子B0,B1...Bk-1 的根組成

二項佇列操作

二項佇列結構

struct BinNode{
    int Elem;
    Position LeftChild;
    Position NextSibling;
};
struct Collection{
    int CurrentSize;
    BinTree TheTrees[MaxSize];
};

查詢最小元

通過搜尋所有的樹根,O(logN)找到

合併

二項佇列按照高度給二項樹排序放在陣列中。

合併相同大小的二項樹例程

BinTree CombineTrees(BinTree T1,BinTree T2){
    if(T1->Elem > T2->Elem)
        return CombineTrees(T2,T1);
    T2->NextSibling = T1->LeftChild;
    T1->LeftChild = T2;
    return T1;
}

二項佇列的合併類似於加法進位,有8種情況考慮,T1,T2以及Carry分別是二項佇列H1和H2的樹,上一次合併形成的樹。對應的樹合併後放入H1中,清空H2

BinQueue Merge(BinQueue H1, BinQueue H2){
    BinTree T1,T2,Carry = NULL;
    int i,j;
    H1->CurrentSize += H2->CurrentSize;
    for (int i = 0,j=1; j < H1->CurrentSize; ++i,j*=2) {
        T1 = H1->TheTrees[i];
        T2 = H2->TheTrees[i];
        switch (!!T1 + 2*!!T2 + 4*!!Carry){
            case 0://沒有樹
            case 1://只H1存在
                break;
            case 2://只H2存在
                H1->TheTrees[i] = H2;
                H2->TheTrees[i] = NULL;
                break;
            case 3://H1和H2存在
                Carry = CombineTrees(T1,T2);
                H1->TheTrees[i] = H2->TheTrees[i] = NULL;
                break;
            case 4://H1和H2不存在,Carry存在
                H1->TheTrees[i] = Carry;
                Carry = NULL;
                break;
            case 5://H1和Carray
                Carry = CombineTrees(T1,Carry);
                H1->TheTrees[i] = NULL;
                break;
            case 6://H2和Carry
                Carry = CombineTrees(T2,Carry);
                H2->TheTrees[i] = NULL;
                break;
            case 7://H1,H2和Carry都存在
                H1->TheTrees[i] = Carry;
                Carry = CombineTrees(T1,T2);
                H2->TheTrees[i] = NULL;
                break;
        }
    }
    return H1;
}

插入

插入是特殊情況的合併,建立單節點樹,執行合併

BinQueue Insert(int Item, BinQueue H) {  
    BinTree NewNode;    //二項樹B0  
    BinQueue OneItem;   //只有B0的二項佇列  
    NewNode = malloc(sizeof(struct BinNode));  
    NewNode->Elem = Item;  
    NewNode->LeftChild = NewNode->NextSibling = NULL;  
    OneItem = Init(); 
    OneItem->CurrentSize = 1;  
    OneItem->TheTrees[0] = NewNode;  
    return Merge(H, OneItem);   //合併單節點的二項樹構成的二項佇列與H  
}  

刪除最小元

首先確定最小根的下標。從原二項佇列中刪除當前二項樹稱H,二項樹刪除最小元后形成新的二項佇列DeletedQueue,合併原二項佇列和新的二項佇列

int DeleteMin(BinQueue H){
    int i,j;
    int index;// array index;
    BinQueue DeletedQueue;
    Position DeletedTree, OldToot;
    int Min;
    Min = 9999;
    for (int k = 0; k < 10; ++k) {
        if(H->TheTrees[k] && H->TheTrees[k]->Elem < Min){
            index = k;
            Min = H->TheTrees[k]->Elem;
        }
    }
    DeletedTree = H->TheTrees[index];//指向當前最小根節點二項樹
    OldToot = DeletedTree;
    DeletedTree = DeletedTree->LeftChild;
    free(OldToot);

    DeletedQueue = Init();
    DeletedQueue->CurrentSize = (1<<index) - 1;
    for(j = index - 1; j >= 0; j--){
        DeletedQueue->TheTrees[j] = DeletedTree;
        DeletedTree = DeletedTree->NextSibling;
        DeletedQueue->TheTrees[j]->NextSibling = NULL;
    }
    H->TheTrees[index] = NULL;
    H->CurrentSize -=DeletedQueue->CurrentSize + 1;
    Merge(H,DeletedQueue);
    return Min;
}

總結

優先佇列有幾種實現,標準的二叉堆,以及左式堆,斜堆和二項佇列。在實現上不同,二叉堆利用了陣列的特性,後三者通過指標。左式堆和斜堆類似於伸展樹和AVL樹,都是遞迴的完美例項,需要仔細咀嚼精華。

相關文章