優先隊列與Heap的小結

優先隊列是一種使用比較普遍的數據結構。不一樣於通常的隊列,優先隊列的元素都具備優先級,優先級高的元素會被優先選取。利用這個特色,咱們能夠根據元素值的大小來設置優先級,值最大/最小的擁有最高的優先級。這樣,咱們就能夠快速地獲取隊列中最大/最小的元素。這篇文章我將着重比較三種常見的,構造優先隊列的數據結構 - Binary Heap(二叉堆), Leftist Heap(左傾堆)和Skew Heap(斜堆)。html

這篇文章的完成借鑑了不少網上的資料,其中最主要的是這幾篇:
二叉堆(一)之 圖文解析 和 C語言的實現
Priority Queues (Heaps)
數據結構與算法(五)node

Binary Heap(二叉堆)

我這裏直接給出維基百科中關於Binary Heap的解釋:算法

二叉堆是一種特殊的堆,二叉堆是徹底二叉樹或者是近似徹底二叉樹。二叉堆知足堆特性:父節點的鍵值老是保持固定的序關係於任何一個子節點的鍵值,且每一個節點的左子樹和右子樹都是一個二叉堆。數組

當父節點的鍵值老是大於或等於任何一個子節點的鍵值時爲最大堆。當父節點的鍵值老是小於或等於任何一個子節點的鍵值時爲最小堆。數據結構

我這裏補充一下徹底二叉樹的概念:徹底二叉樹是指除了樹的最下層外,全部層的節點都達到最大,而且最下層不滿的節點都位於左支。函數

在構造Binary Heap時,咱們通常都使用數組而不是鏈表(網上也不多有用鏈表實現Binary Heap的資料),我這裏也用數組來構造Binary Heap。Binary Heap分爲最大堆和最小堆,在本文我只介紹最小堆,最大堆和最小堆的實現基本同樣。spa

Min Heap(最小堆)

咱們先來看個最小堆的例子:code

最小堆實例

咱們能夠看到,全部節點的值都小於等於其子節點的值。這裏須要注意,構造Binary Heap時,咱們能夠用兩種形式的數列1)使用index = 0的元素;2)不使用index = 0的元素htm

上面的例子使用的是第二種形式,下面全部關於最小堆的代碼是基於第一種形式。這兩種形式有一個很小的區別,在1)中:blog

  1. index = x節點的左子節點的index = 2 * x + 1

  2. index = x節點的右子節點的index = 2 * x + 2

  3. index = x節點的父節點的index = floor((x - 1) / 2)

在2)中:

  1. index = x節點的左子節點的index = 2 * x

  2. index = x節點的右子節點的index = 2 * x + 1

  3. index = x節點的父節點的index = floor(x / 2)

Min Heap通常支持插入,刪除,建立和查找函數。咱們這裏詳細講解下插入(建立)和刪除。

插入

插入能夠分爲兩步:
第一步,在數列的末尾添加須要插入的值。
第二步,比較該節點與其父節點的大小,若是比其父節點大,插入結束;若是比其父節點小,交換這兩個節點並重復步驟2直到插入結束或者該節點成爲根節點。

咱們經過下面這個示意圖來看看具體是怎樣將14插入到最小堆的的:

最小堆的插入操做

瞭解瞭如何插入後,咱們分析下插入操做的時間複雜度:

  1. 在最好的狀況下,插入節點的值大於其父節點,咱們不須要對堆進行調整,插入完成,時間複雜度爲O(1)。

  2. 在最壞的狀況下,插入的節點值比根節點還小,那麼咱們須要將該節點一直交換到根節點,所以時間複雜度是O(h),其中h是最小堆的高度。根據徹底二叉樹的性質,有N個節點的徹底二叉樹的高度爲log(N + 1),所以O(h) = O(log(N + 1)) = O(logN)。關於徹底二叉樹高度的證實請參考這篇博文:二叉查找樹(一)之 圖文解析 和 C語言的實現

綜上,最小堆的插入算法平均時間複雜度是O(logN)。

下面是插入操做的代碼:

/****************************************************************************************
 * Insert Operation
 ***************************************************************************************/
void min_heap_up_update(int key) {
    int p_node_index, new_node_index;
    
    /* set inserted node's init index */ 
    new_node_index = heap_size;
    /* get inserted node's father node's index and key */ 
    p_node_index = (new_node_index - 1 ) / 2;

    while (new_node_index > 0) {
        if (min_heap[p_node_index]<= key) {
           break; 
        } else {
            /* please note we do not swap key between father node and child 
             * node, we only assign father node's key to its child node's key */ 
            min_heap[new_node_index] = min_heap[p_node_index];
            new_node_index = p_node_index;
            p_node_index = (p_node_index - 1) / 2;
        }
    }
    /* at his point, we assign key to the inserted node */
    min_heap[new_node_index] = key;
}

void min_heap_insert(int key) {
    if (heap_size == MAX_SIZE) {
        printf("Min Heap is full...\n"); 
        return;
    }
    
    min_heap[heap_size] = key; 
    min_heap_up_update(key);
    heap_size++; 
}

在代碼的實現上,咱們並無不斷的交換符合條件的父節點和子節點,咱們只是在最後肯定了新節點的位置後,咱們纔將這個節點的key設置爲咱們須要的key。在最小堆的代碼中,咱們用 heap_size 這個全局變量表示當前堆的大小,用 min_heap[]
這個全局數組表示最小堆。

刪除

這裏的刪除指的是刪除最小值,也就是刪除根節點。刪除的操做和插入的操做相似,只是插入是經過向上更新最小堆,而刪除是經過向下更新最小堆。刪除操做能夠分爲兩步:
第一步,用最小堆的最後一個節點去取代根節點。
第二步,用更新後的第一個節點與其較小的子節點比較,若是該節點比其較小的子節點小,刪除操做結束;不然交換這兩個節點並重復步驟2直到刪除操做結束。

刪除操做的時間複雜度和插入同樣:

  1. 在最好的狀況下,刪除的時間複雜度爲O(1) - 好比整個最小堆的節點都有相同的key,咱們只須要比較一次。

  2. 在最壞的狀況下,咱們須要將根節點交換到堆的最下一層,所以時間複雜度是O(logN)。

綜上,最小堆的刪除算法平均時間複雜度是O(logN)。

下面是刪除操做的代碼:

/****************************************************************************************
 * Delete Operation
 ***************************************************************************************/
void min_heap_down_update(int position) {
    int c_node_index, cur_node_index, cur_node_val;
    
    cur_node_index = position;
    cur_node_val = min_heap[cur_node_index];
    c_node_index = 2 * cur_node_index + 1;
    
    while (c_node_index < heap_size) {
        /* if node has two children we choose the one with smaller key */
        if ((c_node_index < heap_size - 1) && (min_heap[c_node_index] > min_heap[c_node_index + 1])) 
            c_node_index = c_node_index + 1;

        if (cur_node_val <= min_heap[c_node_index]) { 
            break;
        } else {
            min_heap[cur_node_index] = min_heap[c_node_index];
            cur_node_index = c_node_index;
            c_node_index = 2 * c_node_index + 1;
        }
    }
    min_heap[cur_node_index] = cur_node_val; 
}

void min_heap_remove() {
    if (heap_size == 0) {
        printf("Min Heap is empty...\n");
        return;
    }
    
    min_heap[0] = min_heap[heap_size - 1];  
    min_heap_down_update(0);
    heap_size--;
}

同插入操做相似,在代碼中咱們並無不斷的交換父子節點的值,只是在刪除結束後,咱們才更新節點的值。

構造

咱們能夠簡單的經過不斷的插入節點來完成最小堆的構造,根據插入操做的複雜度,要構造一個N個節點的最小堆須要的時間複雜度是O(N*log(N))。有沒有更快速的方法來構造最小堆呢?方法是有的,咱們來看看如何使用O(N)的時間來構造一個包含N個節點的最小堆。

插入的方法是自下而上的構造最小堆,咱們這裏的方法是自上而下的構造最小堆。要知足最小堆成立,咱們須要保證全部的節點往下都構成最小堆。所以,咱們能夠將須要添加到最小堆的數按任意順序放入最小堆的數組(此時不是最小堆),而後經過不斷的調整來使其成爲最小堆。這麼作有一個好處,咱們只須要調整前N/2的節點。爲何呢?由於堆中的後N/2的節點是葉節點,它們已是最小堆了,所以咱們只須要調整前N/2的節點便可將該堆調整成最小堆。

咱們來分析下時間複雜度,我這裏直接引用數據結構與算法(五)中的內容:

clipboard.png

根據計算,這麼作能夠達到O(N)的時間複雜度。

下面是最小堆建造的代碼:

for (int i = heap_size / 2; i >=0; i--) 
    min_heap_down_update(i);

min_heap_down_update()是在刪除操做中實現的。

Leftist Heap(左傾堆)

Skew Heap(斜堆)

相關文章
相關標籤/搜索