挑戰408——數據結構(17)——優先隊列與二叉堆

優先隊列

我們回顧之前我們學過的隊列,隊列中的元素按照特定的順序進行儲存,並只能先進先出。然而,在現實生活中,我們卻想把元素按照一定的優先級儲存起來。舉個現實中的例子:

  • 我們平時坐高鐵,會有所謂的頭等艙,二等艙,普通艙。
  • 在銀行排隊,總會有vip客戶提前辦理業務

所謂優先隊列(priority queue),就是把元素按照一定的優先級儲存起來,而不是根據特定的順序。因此,它與我們之前接觸的基於位置的數據結構有着本質的區別。因爲這裏並沒有所謂的特定位置(類似於數組的下標)這個概念。
假設這裏存在一個優先隊列P,那麼優先隊列P應該有下面三個基本操作:
在這裏插入圖片描述
優先級隊列也有一些較少的的基本函數:
在這裏插入圖片描述
可以看到,優先隊列比我們傳統的隊列簡單多了,因爲我們不用考慮元素的位置問題,也就是說我們不用去實現複雜的insert,add功能。我們要做的只是,enqueue跟dequeue操作。原理如下
在這裏插入圖片描述

二叉堆(binary heap)

那麼怎麼去實現這個優先級隊列呢?可能第一的反應就是使用我們的鏈表吧,一個存儲優先級一個存儲我們的值,但是作爲擴展,我們介紹一個更方便的結構——二叉堆.。
堆,是一種基於樹的結構,它具有以下屬性: 父母的優先權要高於他們的孩子。
而一般的,我們有以下兩種形式的堆:小根堆(左),大根堆(右)
在這裏插入圖片描述
注意:小根堆的根一定是堆中最小的元素,而且他們的父親一定是要小於他們的任一個兒子。大根堆中的根一定是堆中的最大的元素。且他們的父親一定是要大於他們的任一個兒子。如下圖,顯然左邊不是小堆右邊纔是(因爲左邊的12比11大):
在這裏插入圖片描述
當然,如果我們再仔細觀察,我們可以發現,每一個堆除了最底層,都是充滿的。因此,堆是一種完全二叉樹(complete binary trees)。那麼什麼又是完全二叉樹?我們從它的命名來解釋:

  • 完全(complete):除了最後一層,其他層都是滿的。
  • 二叉(binary):每個父親節點都只有兩個兒子。

下圖是一個堆,也是一個完全二叉樹:
在這裏插入圖片描述

二叉堆的存儲

那麼應該如何來存儲我們的二叉堆。假設我們有下面的這個二叉堆:

我們看到這個,也許我們第一反應是用一個基於節點的方式儲存(因爲這樣確實很像)在這裏插入圖片描述
我們看到這個,也許我們第一反應是用一個基於節點的方式儲存(因爲確實長得很像!):
在這裏插入圖片描述
是的,這就是一個很正常也是最容易想到的方式。然而,科學家卻發現使用基於數組的方式能更好的存儲我們的二叉堆。我們把根(root),放在下標爲1的位置(而不是0),這是爲了我們能從數學的角度更好的理解這個原理。 如下圖所示:
在這裏插入圖片描述
在這裏插入圖片描述
爲什麼是這樣存放我們的數組呢?因爲用數組表示二叉堆,使得我們確定父親與孩子之間的問題就變成了簡單的算術問題:
對於每一個位於下標爲 i 的元素來說

  • 它的左孩子一定在 2i 的位置
  • 右孩子一定在 2i + 1 的位置
  • 父親一定在[i / 2]的位置(向上取整)

我們舉例子,上面的元素12,下標爲4,所以它的左孩子的下標就是 2 x 4 = 8.也就是上圖的22元素。它的右孩子同理。它的父親爲 4 / 2 = 2的位置,也就是元素10。

二叉堆的基本操作

回到二叉堆的基本功能上:
現在,我們可以從堆的角度去解決這個問題:

peek()

只需返回堆中的第一個元素即可。 顯然樹根的位置是固定的,算法複雜度爲O(1)。

return heap[1];
enqueue(k)

在堆中插入元素。在這裏假設我們要在堆中插入一個元素爲9.我們應該要經過下面的一些步驟:

  1. 我們先把元素插入到舊數組元素的尾部,也就是heap[heap.size() + 1]的位置。
  2. 執行“冒泡”(bubble up)或“上堆”(up-heap)操作:比較添加的元素與其父親, 如果按正確的順序,停止操作,如果不是,交換並重復步驟2。(這一步實際上就是把插入的元素正確的放入堆中的位置)。

下面,假設在下列的堆中插入一個元素9
在這裏插入圖片描述在這裏插入圖片描述
首先,們把新元素插入到舊數組的尾部,也就是堆中的空白位置(注意,這不是隨意放入哪個空白的位置,因爲我們完全可以通過下標計算父親是誰,比如9放在數組的尾部,就是位置10,所以他應該在位置爲5的節點下面。並且是左節點),也就是heap[heap.size() + 1]的位置,在這個例子中就是我們的下標10:
在這裏插入圖片描述
然後,根據堆的屬性,我們找到下標爲10的元素的父親位於哪個位置。也就是計算10 / 2 = 5,也就是位於5的元素就是新插入元素的父親,那麼它符不符合小堆的要求?11 > 9 顯然是不符合,所以我們把他們進行交換位置。如下面兩幅圖所示:
在這裏插入圖片描述
在這裏插入圖片描述
這個時候我們再對比5號位置的值與它的父親位置的值,這個時候我們計算 5 / 2 = 2.5,可是這裏並沒有下標爲2.5的元素,我們採取的是向下取整(其實設置爲int型就會自動轉換的),所以下標爲2的元素10,仍然大於9,於是我們再交換,(這一步實際上是上一步的重複):
在這裏插入圖片描述
重複上述判斷,發現此時位置正確。

對於算法複雜度,我們通常都討論的是最壞情況下的算法複雜度。而插入元素的操作,顯然是個遞歸的過程,(這與我們之前討論的合併排序一樣)。非遞歸操作並不需要循環執行,我們只需要關心交換節點的操作要做多少次。顯然,最差情況下,我們只需要看這顆樹最多可以有多少層即可。所以,算法複雜度爲O(log N)。平均複雜度爲O(1).

dequeue()

這個操作是要移除堆中的堆頂(也就是樹根),那麼移除之後,需要整個堆都需要重組(因爲移除了堆頂後就破壞了堆的結構,需要找一個新的節點來做這個堆頂)。具體操作如下:

  1. 當我們刪除根的時候,我們仍然需要保留一個完全樹:於是我們用最後一個元素替換根。
  2. 使用下堆(down-heap)操作,尋找新符合條件的新的根:將根與它的孩子們進行比較,如果他們的順序正確,操作停止。否則將根與兩個孩子中最小的那個進行交換,然後繼續重複步驟2.
  3. 要時刻注意該節點是否存在孩子,如果沒有孩子或者只有一個,那麼就不用執行比較操作。那麼如何判斷呢?可以這樣想,如果有該節點有右孩子,那麼它的左孩子也必定存在!!(在實現的時候,可以作爲遞歸的simple case)。

假設我們的初始堆如下:
在這裏插入圖片描述
那麼移除了堆頂後,情況是這樣的:
在這裏插入圖片描述
此時將最後一個元素(此時它的位置當然是在 heap[heap.size()] 處)移動到堆頂處,準備我們的down-heap操作。(注意,移動了堆頂以後,不要忘記了堆的大小是要減一的)
在這裏插入圖片描述
遞歸地將與它的較小的節點進行比較,如果符合要求就交換(這裏指的是13和8)
在這裏插入圖片描述
如果有必要,那麼我們繼續保持交換,直到它再也沒有更多的孩子跟它比較了,我們也就完成工作了!
在這裏插入圖片描述
至此,簡單的二叉堆的基本操作就完成了,但是這有個問題。這些操作都是建立在已有堆的操作上的。但是如果給的是一堆無關的數字呢?顯然我們要學會怎麼建立一個二叉堆!

下次再說,如何建立二叉堆與如何完成堆排序。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章