堆和優先佇列

universe_ant發表於2016-12-09

0、引言

在許多問題中,當對資料集進行頻繁的插入和刪除操作時,往往需要快速確定最大或最小的元素。處理這種問題的方法之一,就是使用一個已排好序的資料集。通過這種方法,最大或最小元素總是處在資料集的頭部(這取決於使用升序還是降序排列)。然而,將資料集一遍又一遍地進行排序的代價是非常高的。並且很多情況下,將元素排序並不是操作的目的,最終我們可能在真正要做的工作之外做了很多其他的工作。想要快速地找到最大或者最小元素,只需要讓元素儲存在可以找到它的位置上就行。堆和優先佇列就是一種處理這種問題的有效方法。

堆:它是一種樹型組織,使我們能夠迅速確定包含最大值的結點。維持一棵樹的代價低於維持一個有序資料集的代價。同樣,我們可以通過堆快速地找到包含最小值的元素。

優先佇列:它是一個從堆自然衍生而來的資料結構。在優先佇列中,資料儲存在一個堆中,這樣我們能夠迅速確定下一個最高優先順序的結點。所謂元素的"優先順序"在不同的問題中意義也不相同。


1、堆的描述

堆是一棵二叉樹,通常其子結點儲存的值比父結點的值小。所以根結點是樹中最大的結點。同樣,我們也可以讓堆向另一種方向發展,即每個子結點儲存的值比父結點的值大。這樣根結點就是樹中最小的結點。這樣的二叉樹是區域性有序的,任何一個結點與其兄弟結點之間都沒有必然的順序關係,但它與其父子結點有大小關係。子結點比父結點小的堆稱為最大值堆,這是因為根結點儲存該樹所有結點的最大值。反之,子結點比父結點大的堆稱為最小值堆。

堆是左平衡的樹,所以隨著結點的增加,樹會逐級從左至右增長。因此對於堆來說,一個比較好的表示左平衡二叉樹的方式是,將結點通過水平遍歷的方式連續儲存到一個陣列中。假設有一個零索引陣列,這表示陣列中處於位置i處的結點,其父結點位於⌊i-1⌋/2,計算中要忽略(i-1)/2的小數部分。其左結點和右結點分別位於2i+1和2i+2位置上。這樣的組織結構對於堆來說非常重要,因為通過它我們能迅速地定位堆的最後一個結點:最後一個結點指處於樹中最深層最右端的結點。這在實現某些堆操作時非常重要。


2、堆的介面定義

heap_init

——————

void heap_init(Heap *heap, int (*compare)(const void *key1, const void *key2), void (*destroy)(void *data));

返回值:無

描述:初始化堆heap。在對堆進行其他操作之前必須首先呼叫初始化函式。在堆形成的過程中,函式指標compare會被堆的各種操作呼叫,用來比較堆中的結點。如果堆為最大值堆,當key1>key2時,函式返回1;當key1=key2時,函式返回0;當key1<key2時,函式返回-1。如果堆為最小值堆,那麼函式的返回結果相反。引數destroy是一個函式指標,通過呼叫其所指向的函式來釋放動態分配的記憶體空間。例如,如果一個堆包含使用malloc動態分配記憶體的資料,那麼當堆銷燬時,destroy會呼叫free來釋放記憶體空間。當一個結構化資料包含若干動態分配記憶體的資料成員時,destroy應該指向一個使用者定義的函式來釋放資料成員和結構本身的記憶體空間。如果堆中的資料不需要釋放,那麼destroy應該指向NULL。

複雜度:O(1)


heap_destroy

——————

void heap_destroy(Heap *heap);

返回值:無

描述:銷燬堆heap。在呼叫heap_destroy之後不再允許進行其他操作,除非再次呼叫heap_init。heap_destroy會刪除堆中所有的結點,在刪除的同時呼叫heap_init中destroy所指向的銷燬函式(前提是此函式指標不為NULL)。

複雜度:O(n),其中n是堆中結點的個數。


heap_insert

——————

int heap_insert(Heap *heap, const void *data);

返回值:如果插入元素成功,返回0;否則,返回-1。

描述:向堆heap中插入一個結點。新結點包含一個指向data的指標,只要結點仍然存在於堆中,此指標就一直有效。與data相關的記憶體空間將由函式的呼叫者來管理。

複雜度:O(lg n),其中n是堆中結點的個數。


heap_extract

——————

int heap_extract(Heap *heap, void **data);

返回值:如果結點釋放成功,返回0;否則,返回-1。

描述:從堆heap中釋放堆頂部的結點。返回時,data指向被釋放結點中儲存的資料。與data相關的記憶體空間將由函式的呼叫者來管理。

複雜度:O(lg n),其中n是堆中結點的個數。


heap_size

——————

int heap_size(const Heap *heap);

返回值:堆中的結點個數。

描述:獲取堆heap結點個數的巨集。

複雜度:O(1)


3、堆的實現與分析

這裡的堆使用二叉樹實現,其結點按照樹的層次結構存放在一個陣列中。結構Heap是堆的資料結構。此結構包含4個成員:size指明堆中結點的個數;compare和destroy是用於封裝傳入heap_init的函式指標;tree是堆中儲存結點的陣列。

// 堆的標頭檔案

/* heap.h */

#ifndef HEAP_H
#define HEAP_H

/* Define a structure for heaps. */
typedef struct Heap_
{
    int size;
    int (*compare)(const void *key1, const void *key2);
    void (*destroy)(void *data);
    void **tree;
} Heap;

/* Public Interface. */
void heap_init(Heap *heap, int (*compare)(const void *key1, const void *key2), void (*destroy)(void *data));

void heap_destroy(Heap *heap);

int heap_insert(Heap *heap, const void *data);

int heap_extract(Heap *heap, void **data);

#define heap_size(heap) ((heap)->size)

#endif // HEAP_H

// 堆的實現

/* heap.c */

#include <stdlib.h>
#include <string.h>

#include "heap.h"

/* Define private macros used by the heap implementation. */
#define heap_parent(npos) ((int)(((npos) - 1) / 2))

#define heap_left(npos) (((npos) * 2) + 1)

#define heap_right(npos) (((npos) * 2) + 2)

/* heap_init */
void heap_init(Heap *heap, int (*compare)(const void *key1, const void *key2), void (*destroy)(void *data))
{
    /* Initialize the heap. */
    heap->size = 0;
    heap->compare = compare;
    heap->destroy = destroy;
    heap->tree = NULL;

    return;
}

/* heap_destroy */
void heap_destroy(Heap *heap)
{
    int i;

    /* Remove all the nodes from the heap. */
    if(heap->destroy != NULL)
    {
        for(i = 0; i < heap_size(heap); i++)
        {
            /* Call a user-defined function to free dynamically allocated data. */
            heap->destroy(heap->tree[i]);
        }
    }

    /* Free the storage allocated for the heap. */
    free(heap->tree);

    /* No operations are allowed now, but clear the structure as precaution. */
    memset(heap, 0, sizeof(Heap));

    return;
}

/* heap_insert */
int heap_insert(Heap *heap, const void *data)
{
    void *temp;
    int ipos, ppos;

    /* Allocate storage for the node. */
    if((temp = (void **)realloc(heap->tree, (heap_size(heap) + 1) * sizeof(void *))) == NULL)
    {
        return -1;
    }
    else
    {
        heap->tree = temp;
    }

    /* Insert the node after the last node. */
    heap->tree[heap_size(heap)] = (void *)data;

    /* Heapify the tree by pushing the contents of the new node upward. */
    ipos = heap_size(heap);
    ppos = heap_parent(ipos);

    while(ipos > 0 && heap->compare(heap->tree[ppos], heap->tree[ipos]) < 0)
    {
        /* swap the contents of the current node and its parent. */
        temp = heap->tree[ppos];
        heap->tree[ppos] = heap->tree[ipos];
        heap->tree[ipos] = temp;

        /* Move up one level in the tree to continue heapifying. */
        ipos = ppos;
        ppos = heap_parent(ipos);
    }

    /* Adjust the size of the heap to account for the inserted node. */
    heap->size++;

    return 0;
}

/* heap_extract */
int heap_extract(Heap *heap, void **data)
{
    void *save, *temp;
    int ipos, lpos, rpos, mpos;

    /* Do not allow extraction from an empty heap. */
    if(heap_size(heap) == 0)
        return -1;

    /* Extract the node at the top of the heap. */
    *data = heap->tree[0];

    /* Adjust the storage used by the heap. */
    save = heap->tree[heap_size(heap) - 1];

    if(heap_size(heap) - 1 > 0)
    {
        if((temp = (void **)realloc(heap->tree, (heap_size(heap) - 1) * sizeof(void *))) == NULL)
        {
            return -1;
        }
        else
        {
            heap->tree = temp;
        }

        /* Adjust the size of the heap to account for the extracted node. */
        heap->size--;
    }
    else
    {
        /* Manage the heap when extracting the last node. */
        free(heap->tree);
        heap->tree = NULL;
        heap->size = 0;
        return 0;
    }

    /* Copy the last node to the top. */
    heap->tree[0] = save;

    /* Heapify the tree by pushing the contents of the new top downward. */
    ipos = 0;
    lpos = heap_left(ipos);
    rpos = heap_right(ipos);

    while(1)
    {
        /* Select the child to swap with the current node. */
        lpos = heap_left(ipos);
        rpos = heap_right(ipos);

        if(lpos < heap_size(heap) && heap->compare(heap->tree[lpos], heap->tree[ipos]) > 0)
        {
            mpos = lpos;
        }
        else
        {
            mpos = ipos;
        }

        if(rpos < heap_size(heap) && heap->compare(heap->tree[rpos], heap->tree[mpos]) > 0)
        {
            mpos = rpos;
        }

        /* When mpos is ipos, the heap property has been restored. */
        if(mpos == ipos)
        {
            break;
        }
        else
        {
            /* Swap the contents of the current node and the selected child. */
            temp = heap->tree[mpos];
            heap->tree[mpos] = heap->tree[ipos];
            heap->tree[ipos] = temp;

            /* Move down one level in the tree to continue heapifying. */
            ipos = mpos;
        }
    }
    return 0;
}


heap_init

堆由heap_init初始化,經過初始化的堆才能進行其他操作。堆的初始化過程比較簡單,它將size成員設定為0;將compare成員指向compare;將destroy成員指向destroy;然後將tree指標設定為NULL。

heap_init的時間複雜度為O(1),因為初始化堆的幾個步驟都能在固定時間內完成。


heap_destroy

堆由heap_destroy進行銷燬。該函式只要用於移除堆中的所有結點。當在heap_init中destroy指標不為NULL時,destroy將指向此函式以便在移除每個結點時呼叫。

heap_destroy的時間複雜度為O(n),其中n為堆中結點的個數。這是由於必須遍歷堆中所有的結點來釋放堆中的資料。如果destroy為NULL,那麼heap_destroy的複雜度為O(1)。


heap_insert

堆由heap_insert向堆中插入結點。函式將結點指向呼叫者傳入的資料。首先,要為新的結點重新分配儲存空間,以保證樹能容納此結點。新插入的結點將首先放到陣列的末尾。此時將破壞堆的固有特性,所以我們必須調整樹的結構,對結點進行重新排列。

在插入結點時,為了重新排列一棵樹,只需要考慮新結點插入的那個分支,因為這是形成堆的區域性開始分支。從新結點開始,將結點向樹的上方層層移動,比較每個子結點和它的父結點。在每一層上,如果父結點與子結點的位置不正確,就交換結點的內容。這個交換過程會不斷進行直到某一層不再需要進行交換為止,或者直到結點到達樹的頂部。最後,通過堆資料結構中的size成員來更新堆的容量。

heap_insert的時間複雜度為O(lg n),其中n為堆中結點的個數。因為在最壞情況下,需要將新結點的內容從樹的底層移動的樹的頂部,這是一個lgn級別的遍歷過程。而剩下的操作都能在固定時間內完成。


heap_extract

堆由heap_extract操作來釋放堆頂部的結點。首先,將data指向將要釋放結點的資料。接下來,儲存最後一個結點的內容,將樹的大小減一,為樹重新分配一個稍小的儲存空間。在確定以上操作都成功之後,將最後一個結點中的內容拷貝到根結點中。顯然,這個過程會破壞堆固有的特性,所以我們必須調整樹的結構,對結點進行重新排列。

在取出結點後,為了重新排列一棵樹,從根結點開始沿樹幹層層向下移動,與結點的兩個子結點進行比較。在每一層上,如果父結點與子結點的位置不正確,就交換結點的內容,此時需要將父結點與較大的那個子結點進行交換。這個交換過程會不斷進行下去直到某一層不再需要進行交換為止,或者直到結點到達一個葉子結點。最後通過遞減堆資料結構中的size成員更新堆的容量。

heap_extract的時間複雜度為O(lg n),其中n為堆中結點的個數。因為在最壞的情況下,需要將新結點中的內容從樹的頂部一直移動到樹的一個葉子結點上,這是一個lgn級別的遍歷過程。而剩下的操作都能在固定時間內完成。


heap_size

這個巨集計算堆中結點的個數。它通過訪問結構Heap的size成員來獲得。

其時間複雜度為O(1),因為訪問結構中的成員是一個執行時間固定的簡單操作。


4、優先佇列的描述

優先佇列將資料按照優先順序順序排列。一個優先佇列由許多有序的元素構成,所以優先順序最高的元素可以有效而快速地確定。例如,我們看一組用來做負載均衡的伺服器,時時觀察它們的使用情況。當連線請求到達時,優先佇列可以告知當前哪個伺服器是處理此連線請求的最佳伺服器。在這種情況下,最空閒的伺服器獲得的優先順序最高,因為它可以最好地處理服務請求。


5、優先佇列的介面定義

pqueue_init

——————

void pqueue_init(PQueue *pqueue, int (*compare)(const void *key1, const void *key2), void (*destroy)(void *data));

返回值:無

描述:初始化優先佇列pqueue。在對優先佇列進行其他操作之前必須首先呼叫初始化函式。在優先佇列的形成過程中,函式指標compare會被優先佇列的各種操作呼叫,用來維持優先佇列的堆特性。如果佇列中大的值有較高的優先順序,那麼當key1>key2時,函式返回1;當key1=key2時,函式返回0;當key1<key2時,函式返回-1。如果相反佇列中小的值有較高的優先順序,那麼函式的返回結果相反。引數destroy是一個函式指標,通過呼叫destroy指向的函式來釋放動態分配的記憶體空間。例如,如果一個優先佇列包含使用malloc動態分配記憶體的資料,那麼當銷燬優先佇列時,destroy會呼叫free來釋放記憶體空間。當一個結構化資料包含若干個動態分配記憶體的資料成員時,destroy應該指向一個使用者自定義的函式來釋放資料成員和結構本身的記憶體空間。如果優先佇列中的資料不需要釋放,那麼destroy應該指向NULL。

複雜度:O(1)


pqueue_destroy

——————

void pqueue_destroy(PQueue *pqueue);

返回值:無

描述:銷燬優先佇列pqueue。在呼叫pqueue_destroy之後不再允許進行其他操作,除非再次呼叫pqueue_init。pqueue_destroy會從優先佇列中移除所有的元素,在刪除每個元素的同時呼叫pqueue_init中的destroy所指向的銷燬函式(前提是此函式指標不為NULL)。

複雜度:O(n),其中n是優先佇列中結點的個數。


pqueue_insert

——————

int pqueue_insert(PQueue *pqueue, const void *data);

返回值:如果插入元素成功,返回0;否則,返回-1。

描述:向優先佇列pqueue中插入一個元素。新元素包含一個指向data的指標,只要結點仍然存在於優先佇列中,此指標就一直有效。與data相關的記憶體空間將由函式的呼叫者來管理。

複雜度:O(lg n),其中n是優先佇列中結點的個數。


pqueue_extract

——————

int pqueue_extract(PQueue *pqueue, void **data);

返回值:如果元素提取成功,返回0;否則,返回-1。

描述:從優先佇列pqueue中提取優先佇列頂部的元素。返回時,data指向已提取元素中儲存的資料。與data相關的記憶體空間將由函式的呼叫者來管理。

複雜度:O(lg n),其中n是優先順序佇列中結點的個數。


pqueue_peek

——————

void *pqueue_peek(const PQueue *pqueue);

返回值:優先佇列中優先順序最高的元素;如果佇列為空,那麼返回NULL。

描述:獲取優先佇列pqueue中優先順序最高元素的巨集。

複雜度:O(1)


pqueue_size

——————

int pqueue_size(const PQueue *pqueue);

返回值:優先佇列中的結點個數。

描述:獲取優先佇列pqueue結點個數的巨集。

複雜度:O(1)


6、優先佇列的實現與分析

我們可以通過很多方法來實現一個優先佇列。最常用而簡單的方法就是維護一個有序資料集。在這個有序資料集中,優先順序最高的元素位於資料集的頭部。然而,插入或提取元素之後必須重新排列資料集,這是一個複雜度為O(n)的操作(n表示資料集元素的個數)。因此,更好的方法就用一個區域性有序的堆來實現優先佇列。我們回憶一下堆的定義,位於堆頂部的結點往往優先順序最高,而且插入或提取資料之後重新排列堆的複雜度僅為O(lg n)。

我們可以通過這種簡單的方式typedef Heap PQueue來實現一個優先佇列。這是因為優先佇列與堆的操作基本相同,優先佇列僅比堆多了一個介面而已。為了實現這些介面,我們只需要將優先佇列的相應操作定義成堆的操作即可。優先佇列中獨有的操作pqueue_peek與pqueue_extract相類似,只是pqueue_peek返回佇列中優先順序最高的元素,而不刪除它。

// 優先佇列的標頭檔案

/* pqueue.h */

#ifndef PQUEUE_H
#define PQUEUE_H

#include "heap.h"

/* Implement priority queues as heaps. */
typedef Heap PQueue;

/* Public Interface */
#define pqueue_init heap_init

#define pqueue_destroy heap_destroy

#define pqueue_insert heap_insert

#define pqueue_extract heap_extract

#define pqueue_peek(pqueue) ((pqueue)->tree == NULL ? NULL : (pqueue)->tree[0])

#define pqueue_size heap_size

#endif // PQUEUE_H


7、優先佇列的示例:包裹分揀

絕大多數快遞服務公司都會提供幾種服務供客戶選擇。通常來說,如果你願意支付更多的快遞費用,那麼你的包裹能夠保證更快地到達。因為很多大的快遞服務公司每天要處理數以百萬計的包裹,所以將這些包裹按照優先順序排序是很重要的。這在投遞的人力物力有限的情況下尤為重要。在這種情況下,具有高優先順序的包裹往往優先投遞。例如,一架用於投遞服務的飛機,如果它在某個繁忙大城市的中心機場每天只跑一個往返,那麼第二天就要求投遞的包裹在當天就應該裝上飛機。

確保包裹能夠按照正確的優先順序順序送達到指定的目的地的方法是,將包裹的資訊按照正確的優先順序順序儲存在一個優先順序佇列中。包裹分揀過程首先掃描包裹的資訊,並將資訊錄入系統中。對於每個掃描過的包裹,其資訊將會按照優先順序順序儲存到優先佇列中,以便當包裹在系統中傳遞時,具有最高優先順序的包裹將首先投遞。

下面的程式碼中列舉了兩個函式get_parcel和put_parcel,它們都是用來操作一個包含包裹資訊Parcel的優先佇列。Parcel在parcel.h中定義,此處沒有列舉出來,其中主要包括一些包裹資訊。一個分揀器呼叫put_parcel將一個包裹資訊載入到系統中。Parcel中傳遞給put_parcel的一個成員變數代表優先序號。put_parcel將一個包裹插入一個優先佇列中,並按照優先順序找到它在佇列中的位置。當分揀器準備在系統中傳遞下一個包裹時,它會呼叫get_parcel。函式get_parcel會取到最高優先順序的包裹,這樣就能保證包裹按照正確的順序處理。

優先佇列是用來管理包裹的最佳方法,因為某些場合,我們只關心下一個優先順序最高的包裹是哪一個。這樣可以避免維護包裹完全有序的系統開銷。get_parcel和put_parcel的時間複雜度都為O(lg n),因為這兩個函式分別只呼叫了複雜度為O(lg n)的函式pqueue_extract和pqueue_insert。

// 包裹分揀函式的實現

/* parcels.c */

#include <stdlib.h>
#include <string.h>

#include "parcel.h"
#include "parcels.h"
#include "pqueue.h"

/* get_parcel */
int get_parcel(PQueue *parcels, Parcel *parcel)
{
    Parcel *data;

    if(pqueue_size(parcels) == 0)
    {
        /* Return that there are no parcels. */
        return -1;
    }
    else
    {
        if(pqueue_extract(parcels, (void **)&data) != 0)
        {
            /* Return that a parcel could not be retrieved. */
            return -1;
        }
        else
        {
            /* Pass back the highest-priority parcel. */
            memcpy(parcel, data, sizeof(Parcel));
            free(data);
        }
    }
    return 0;
}

/* put_parcel */
int put_parcel(PQueue *parcels, const Parcel *parcel)
{
    Parcel *data;

    /* Allocate storage for the parcel. */
    if((data = (Parcel *)malloc(sizeof(Parcel))) == NULL)
        return -1;

    /* Insert the parcel into the priority queue. */
    memcpy(data, parcel, sizeof(Parcel));

    if(pqueue_insert(parcels, data) != 0)
        return -1;

    return 0;
}


相關文章