深入淺出資料結構C語言版(15)——優先隊列(堆)

來源:互聯網
上載者:User

標籤:city   路徑   完成   div   element   heap   操作   https   不為   

  在普通隊列中,元素出隊的順序是由元素入隊時間決定的,也就是誰先入隊,誰先出隊。但是有時候我們希望有這樣的一個隊列:誰先入隊不重要,重要的是誰的“優先順序高”,優先順序越高越先出隊。這樣的資料結構我們稱之為優先隊列(priority queue),其常用於一些特殊應用,比如作業系統控制進程的發送器。

 

  那麼,優先隊列該如何?呢?我們可以很快給出三種解決方案。

  1.使用鏈表,插入操作選擇直接插入到表頭,時間複雜度為O(1),出隊操作則遍曆整個表,找到優先順序最高者,返回並刪除該結點,時間複雜度為O(N)。

  2.使用鏈表,鏈表中元素按優先順序排序,插入操作需為插入結點找到準確位置,時間複雜度為O(N),出隊操作則直接返回並刪除表頭,時間複雜度為O(1)。

  3.使用二叉尋找樹,插入操作時間複雜度為O(logN),出隊操作則返回樹中最大(或最小,取決於優先順序定義)結點並刪除,時間複雜度亦為O(logN),但是出隊一定會使樹趨向於不平衡。

 

  如果決定使用鏈表,那麼就必鬚根據插入操作和出隊操作的比例,決定用方法1還是方法2。

  如果決定使用二叉尋找樹,實際上有點“殺雞用牛刀”,因為它支援的操作遠不止插入和出隊(即刪除最大結點或最小結點)。而且一個有N個結點的二叉樹有2N個指標域,但只會用掉N-1個(除了根結點,每個結點必有且只有一個指向自身的指標),也就是說必然有N+1個指標域是NULL,即“浪費”掉了。當然,它的時間複雜度比較均衡。

  不過今天我們將使用一種新的資料結構來實現優先隊列,其同樣可以以O(logN)實現插入與出隊,而且不需要用到指標,這種資料結構就叫——二元堆積。

  在討論二元堆積之前,我們先決定一下我們對優先順序的設定,我們假定元素的優先順序為正整數,並且值越小的越優先(這對於我們之後實現二元堆積可以帶來一絲方便)。

 

 

  二元堆積在邏輯結構上就是一棵完全二叉樹,而完全二叉樹即符合下述條件的二叉樹:

  1.除去最底層(即深度最大)的結點後,是一棵滿二叉樹

  2.最底層的結點必須在邏輯上“從左至右”逐一填入,不得有空

  即為一棵完全二叉樹

  

 

  完全二叉樹在編程上最大的特點就是它可以使用數組來儲存(而且不是靠遊標數組),其原理很簡單:令根結點儲存在下標1處,則其他任一結點的父親結點均為自身下標i/2(若i為奇數,則商直接取整數部分,這在代碼上很簡單),任一結點的左孩子下標均為自身下標i*2,右孩子則是i*2+1。

  

  至此,我們確定了兩件事,一,二元堆積就是一棵完全二叉樹;二,完全二叉樹可以用數組儲存,即二元堆積可以用數組儲存。

  我們現在已經實現了說好的“不用指標”,接下來的問題就是如何滿足優先隊列的需求,並且令兩個操作均滿足O(logN)。在那之前,我們先假定好結點結構並給出二元堆積的儲存結構,初始化程式:

 

//二元堆積結構定義struct BinaryHeap {    unsigned int capacity; //capacity表示二元堆積的最大容量    unsigned int size;   //size表示當前二元堆積的大小,即元素個數    unsigned int *heap;  //heap即“數組”,根據初始化時給定的大小初始化};typedef struct BinaryHeap *PriorityQueue;   //PriorityQueue即優先隊列

 

PriorityQueue Initialize(unsigned int capacity){    PriorityQueue pPQueue = (PriorityQueue)malloc(sizeof(struct BinaryHeap));    pPQueue->heap = (unsigned int *)malloc(sizeof(int)*capacity);    pPQueue->capacity = capacity;    pPQueue->size = 0;    pPQueue->heap[0] = 0;  //令heap[0]為0可以避免插入時新元素上濾過頭,習至插入時就明白    return pPQueue;}

 

 

 

 

  那麼,二元堆積是如何滿足優先隊列需求的呢?這就得從二元堆積對結點的要求說起,在二元堆積中結點有且只有兩個要求:

  1.根結點優先順序最高

  2.任一結點優先順序高於其孩子

  中,只有左側的完全二叉樹符合二元堆積要求,右側結點6不符合二元堆積要求

  

 

  接下來帶著這兩個要求,我們看看該如何?對二元堆積的插入。現在,假設我們已經有了如下二元堆積及一個新結點14。

   

  數組儲存如下

  

  首先,我們要確保新結點插入後二元堆積依然是完全二叉樹,保證這一點的方法很簡單,就是讓新結點暫時先插入到完全二叉樹的最後一層最右元素的右邊,直接的說,就是插入到當前數組最後元素的後一個位置。

  然後,我們要讓新結點去往它應在的位置,或者準確點說是應在的層次,這一點的實現非常簡單:令新結點不斷與父結點比較,若新結點優先順序更大,則其與父結點交換位置,直到新結點優先順序不高於父結點為止。這種策略我們稱之為“上濾”(中空結點即新結點14)

   

  

  插入過程數組的示意如下:

  

  知道了插入的思路後,插入的代碼也就不難寫出了:

 

bool Insert(PriorityQueue pPQueue, unsigned int x){    //由於二元堆積的heap[0]是放棄不用的,所以size最大為capacity-1    if (pPQueue->size == pPQueue->capacity - 1 || x == 0 || x > INT_MAX)        return false;    //CurPos即當前位置,初始化為插入後的二元堆積size,即表尾    unsigned int CurPos = ++pPQueue->size;    //不斷地令CurPos對應的父結點與x比較,若大於x則令父結點下濾,等價於令x上濾    //若小於x則退出迴圈,此時CurPos即x應處的位置    for (;pPQueue->heap[CurPos / 2] > x;CurPos /= 2)    {        pPQueue->heap[CurPos] = pPQueue->heap[CurPos / 2];    }    pPQueue->heap[CurPos] = x;    return true;}

  注意到若CurPos為1,即根,則heap[0]將與x比較,為了避免x上濾過頭至heap[0],我們在前面要求了x必須為正整數,而heap[0]則在初始化時設為0,這樣一來heap[0]必小於任一插入元素

   稍加分析就可以看出,插入時的最壞情況也只是新結點上濾到根,此時新結點上濾的路徑就跟向二叉樹中插入了一個葉子結點是類似的,時間複雜度為O(logN)

 

 

 

  現在我們來看看二元堆積是如何?出隊操作的。在二元堆積中要找優先順序最高的結點非常簡單,根結點即是。但是取走了根結點後,該處就成了一個“空結點”,這個“空結點”又該如何處理?簡單的想法是不斷地令“空結點”的孩子中優先順序更高者與“空結點”交換,直至“空結點”到最底層。但這個想法容易出錯,如,空結點最後導致了完全二叉樹屬性的破壞

  

 

  那麼該如何保證二元堆積的完全二叉樹屬性呢?解決方案就是對上述想法稍加改進:根結點刪除後,令二元堆積最後一個結點頂替其位置,而後逐層“下濾”至其優先順序大於其所有孩子為止。這樣一來,二元堆積的完全二叉樹屬性就可以保住。因為這麼做的話,即使“新根結點”下濾到了最底層也不會導致“空結點”的出現從而破壞完全二叉樹屬性。(中空結點即原表尾結點31)

   

  

  

  (出隊操作的數組變化略)

  知道了出隊的思路後,出隊的代碼也就不難寫出了:

unsigned int Dequeue(PriorityQueue pPQueue){    //若堆已空則返回0,0必不為表中元素    if (pPQueue->size == 0)        return 0;        unsigned int root = pPQueue->heap[1];   //root儲存了原堆根,即需要返回的值    unsigned int LastElement = pPQueue->heap[pPQueue->size--];  //LastElement即表尾元素    //令LastElement從根開始下濾,所以CurPos初始化為1,child用於指出CurPos兩個孩子中優先順序更高的那個    unsigned int CurPos = 1;    unsigned int child = CurPos * 2;    while (child <= pPQueue->size)    {        //若child不是最後一個元素,且其兄弟(CurPos的右孩子)優先順序更高,則令child指向CurPos右孩子        if (child != pPQueue->size&&pPQueue->heap[child] > pPQueue->heap[child + 1])            child += 1;        //比較LastElement與CurPos最優先的孩子,若LastElement更優先,則迴圈結束        //否則令CurPos最優先孩子上濾,等價於令LastElement下濾        if (pPQueue->heap[child] < LastElement)        {            pPQueue->heap[CurPos] = pPQueue->heap[child];            CurPos = child;            child = CurPos * 2;        }        else            break;    }    //跳出迴圈後的CurPos即LastElement該處的位置    pPQueue->heap[CurPos] = LastElement;    return root;}

 

  出隊的時間複雜度與入隊(插入)相同,為O(logN)。

 

 

  有了上述代碼,二元堆積就算是基本實現了(Destroy的代碼沒有給出,但實現並不難)。那麼二元堆積,或者說優先隊列(即堆,但不只是二元堆積,還有別的實現方式,均稱為堆或優先隊列)還有什麼別的用處嗎?

  試想一下如果我們將一組需要排序的資料插入到二元堆積去,然後再不斷Dequeue並將得到的元素(即二元堆積的根)插入到普通隊列中,我們是否會得到一個有序的隊列?也就是說,二元堆積可以用來完成排序工作!那麼二元堆積完成排序需要的時間是多少呢?大致是插入時間+出隊時間,即O(N*logN+N*logN),O(N*logN)。這個時間比我們大多數人知曉的冒泡排序、選擇排序要好得多。我們將在之後的博文中完善堆排序的實現方法。

  下面的地址有著二元堆積的簡單實現與實驗,同時展示了二元堆積的排序效果

  https://github.com/nchuXieWei/ForBlog-----BinaryHeap

深入淺出資料結構C語言版(15)——優先隊列(堆)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.