淺入淺出資料結構(15) 優先佇列(堆)

2022-04-28 15:30:14 字數 4035 閱讀 4325

在普通佇列中,元素出隊的順序是由元素入隊時間決定的,也就是誰先入隊,誰先出隊。但是有時候我們希望有這樣的乙個佇列:誰先入隊不重要,重要的是誰的「優先順序高」,優先順序越高越先出隊。這樣的資料結構我們稱之為優先佇列(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 ;

typedef

struct binaryheap *priorityqueue; //

priorityqueue即優先佇列

priorityqueue initialize(unsigned int

capacity)

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

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

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

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

陣列儲存如下

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

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

插入過程陣列的示意如下:

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

bool insert(priorityqueue ppqueue, unsigned int

x) 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)

else

break

; }

//跳出迴圈後的curpos即lastelement該處的位置

ppqueue->heap[curpos] =lastelement;

return

root;

}

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

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

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

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

淺入淺出資料結構(18) 希爾排序

而希爾排序就是 簡單地 將這個道理應用到了插入排序中,將插入排序小小的公升級了一下。那麼,希爾排序是怎麼將這個道理應用於插入排序的呢?我們先來回顧一下插入排序的 void insertionsort int a,unsigned int size 不難看出,在插入排序中,對於每乙個元素,我們都令其執...

淺入淺出資料結構(21) 合併排序

在講解合併排序之前,我們先來想一想下面這個問題如何解決 有兩個陣列a和b,它們都已各自按照從小到大的順序排好了資料,現在我們要把它們合併為乙個陣列c,且要求c也是按從小到大的順序排好,請問該怎麼做?這個問題非常容易解決,我們將a b和c都視為佇列,然後不斷比較a和b的首部,取出其中更小的資料出隊,然...

淺入淺出資料結構(16) 插入排序

從這一篇博文開始,我們將開始討論排序演算法。所謂排序演算法,就是將給定資料根據關鍵字進行排序,最終實現資料依照關鍵字從小到大或從大到小的順序儲存。而這篇博文,就是要介紹一種簡單的排序演算法 插入排序 insertion sort 為了使精力專注於排序演算法本身,而不是對資料的分析 處理,若無特殊說明...