堆(大根堆 小根堆)

2021-10-06 12:36:19 字數 4264 閱讀 1449

堆又可稱之為完全二叉堆。這是乙個邏輯上基於完全二叉樹、物理上一般基於線性資料結構(如陣列、向量、鍊錶等)的一種資料結構。

學習過完全二叉樹的同學們都應該了解,完全二叉樹在物理上可以用線性資料結構進行表示(或者儲存),例如陣列int a[5] = 就可以用來描述乙個擁有5個結點的完全二叉樹。那麼基於完全二叉樹的堆結構自然也可以使用線性結構進行描述。回顧一下這樣表示時,元素的秩之間的關係。若有元素秩為k(k >= 0),則有rank(leftchild) = 2k+1,rank(rightchild) = 2k+2(此處元素的秩從0開始,若從1開始則推導出的公式略有不同,注意這之間的區別)。

堆可分為兩種:大根堆(最大堆)、小根堆(最小堆)。

何為大根堆?顧名思義,大根堆即指在邏輯上的二叉樹結構中,根結點》子結點,總是最大的,並且在堆的每乙個區域性都是如此。例如可以看作為大根堆,而亦可以看作為大根堆。大根堆的根結點在整個堆中是最大的元素。

小根堆的性質與大根堆類似,只不過在二叉樹的結構中,根結點《子結點。例如為小根堆,同樣也是小根堆。小根堆的根結點在整個堆中是最小的元素。

大/小根堆中根結點即是整個序列中最大 /小的元素,那麼從堆中獲取最大/小的元素則非常快速,只要返回序列中的首元素即可。

更一般的,堆處處區域性的有序性,形成了堆整體的有序性。 只要有一處區域性沒有滿足堆的有序性,則可以說堆失序,此時便需要進行相應的調整。

堆的操作呢,無非就是建立、插入、刪除。建立乙個空的堆是非常簡單的操作,而利用乙個已有的序列生成乙個是乙個值得思考的問題;插入和刪除操作,比較簡單;值得思考的是插入元素或者刪除元素之後如何恢復堆的堆序性。

往乙個線性序列中插入乙個元素是比較簡單的,例如陣列a[n],在秩為k(k假設堆使用陣列作為底層的實現,那麼堆的插入又該如何實現呢?是插入到原堆的頭部?還是尾部?亦或是中間? 鑑於原堆已經保持了堆序性,如果插入在頭部,相當於所有元素對應的父結點和子結點都會發生變化,這樣以來會導致整個堆失效;如果插入在中間的某一位置,則會導致插入位置之後的所有元素對應的父結點和子結點發生變化,這樣一來會導致堆的部分失效;如果插入在堆底,則原堆的結構未遭破壞,此時唯一可能不符合堆性質的只是最後乙個元素(剛剛插入的元素)的父結點是否比它大/小,即在這個區域性是否還滿足之前所說的堆序性。

好,現在,可以確定在堆底插入新元素的代價是最小的,那麼接下來就是思考在這種情況下如何恢復堆序性。試想乙個小根堆如下,在其尾部插入元素1,如何調整使其滿足小根堆的性質呢?對,通過將元素2和元素1交換即可得到小根堆。此時回顧以下小根堆的性質便不難看出,在此種情形下,對失序的堆執行以下操作即可恢復其堆序性:若新插入結點小於其父結點,則將兩者交換。

在第三條所闡述的情況中,並沒有對新插入結點的兄弟結點(如果有的話)做任何操作,甚至不需要考慮它,這是為何?假設新插入結點為n,其父結點為f(n),其兄弟結點為b(n),則在原堆(結點n插入之前)中有這樣的關係:f(n) < b(n)。插入結點n後,若n < f(n),則有n < f(n) < b(n)。回顧堆序性的描述,不難知道只要將n與f(n)互換位置就可以使得堆恢復堆序性。

由點及面,假設上述的堆只是另乙個堆heap2中的區域性(3為heap2中的堆底元素,2為3的父結點),通過上一條闡述的操作,我們可以恢復區域性的堆序性,但別忘了,由於原來元素2變成了元素1,此時要繼續檢查元素1以及元素1的父結點、兄弟結點組成的區域性堆是否滿足堆序性,迴圈往復直到插入元素所處的區域性滿足了堆序性。這樣的乙個過程也稱為堆的上濾(percolateup)

**如下:

template

<

typename t>

void

percolateup

(t *s, size_t rank)

else

break;}

}

時間複雜度,o(logn)。不難看出插入的結點在一層一層地向上公升(與父結點互換位置),而完全二叉樹的高度為logn,故其時間複雜度為o(logn)。

從乙個線性序列中刪除乙個元素是簡單的,但對於堆這樣的結構而言,若以比較的方式來刪除某乙個元素並無意義,堆的有序性告訴我們,從堆中刪除掉對頂元素是比較有價值的(時間複雜度o(1))。不妨將堆的刪除操作視為取出堆頂元素。

對於乙個堆heap而言,取出對頂元素只需要刪除掉heap[0]即可,那麼刪除掉首元素之後,原來的堆失去了它的跟結點,整個堆面臨著失效的尷尬境地。與插入操作類似,需要思考乙個問題,如何調整可以使得原堆中其他部分可以依舊保持的原來的結構(各個元素之間的父子關係)?可以在被刪除掉的首元素的位置再插入乙個元素來保持原堆中其他結點的結構不變,可以將這樣的過程視為乙個新的結點替換掉了原來的根結點。

那麼這個新的結點從何而來呢?若選取了中間的某一結點x來替換掉根結點,則會導致x之後的結點全部失效(原來的結構被破壞)。至此,相信大家不難看出,一如插入操作一般,選取堆底元素來替換根結點是最佳的選擇,這使得整個堆依舊可以保持原來的結構。接下來要思考的就是替換之後整個堆是否依舊保持著堆序性。

前面說到堆處處區域性的有序即是堆整體的有序,那麼在替換之後,區域性是否有序便成了判斷堆整體是否有序的依據。分別用n、l(n)、r(n)表示替換之後的根結點、根結點的左孩子、根結點的右孩子。如果n < l(n)且n < r(n),那麼堆序性依舊保持;否則堆序性被破壞。不如將上述的條件寫成如下的形式:如果n < min(l(n), r(n)),那麼堆序性依舊保持;否則堆序性被破壞。此種情形與在堆底插入乙個元素所造成的堆序性破壞略有不同,那麼接下來該思考的是如何調整使得堆的區域性恢復有序性呢?是否可以通過類似於上濾的方案來解決此類失序的問題呢?

還是回到堆序性這個特性上,對於n、l(n)、r(n)這三個結點組成的區域性堆而言,只需保證這三者中的最小值處在根結點這個位置上即可保持堆序性。至此,可以得出若n < min(l(n), r(n))不被滿足,則將n與min(l(n), r(n))呼喚位置即可保持堆序性。同插入處理,在進行一次調整後還需繼續對新形成的區域性堆進行調整,迴圈往復,直至區域性堆恢復堆序性或結點n成為了葉子結點。這樣的乙個過程也稱為堆的下濾(percolatedown)

**如下:

template

void

percolatedown

(t *s, size_t rank, size_t size)

else

break;}

if(rank == lastinternal)

//如果被調整成了最後乙個內部結點,則需要判斷是否有右孩子

}

時間複雜度,漸進意義上o(logn),其內部操作比上濾過程而言要多一些,故其常係數部分略高。

這個問題可以通過建立乙個空堆,然後逐個將序列中的元素插入至堆中來完成,大致地可以得出時間複雜度為o(nlogn)。如果將這樣的操作原地進行,則相當於依次從首元素到尾元素執行上濾操作,暫且將這樣的方式稱之為自上而下的上濾。

是否可以使用下濾來實現這一功能呢?從最後乙個內部結點開始,逐個對所有的內部結點進行下濾操作,完成後就可以得到乙個堆。

在此,不妨將對內部結點下濾的過程視為該內部結點與其左右子堆的合併成乙個堆的過程。設內部結點為n,左右子堆分別用hl和hr來表示。既是堆,那麼hl與hr都滿足堆序性,事實上在從最後乙個內部結點開始時,hl與hr都至多包含乙個元素,只擁有乙個元素的堆自然是有序的。等到所有葉子結點的父結點都與其左右子堆(堆中只有乙個元素)合併後,才會出現hl和hr中包含多個元素的情況,而經過下濾調整的子堆都是滿足堆序性的。至此可以證明通過這樣的方式可以使得整個堆恢復堆序性。不妨將這樣的方式稱之為自下而上的上濾。

時間複雜度為o(n),是的這可能與大家直觀的感覺不一致,證明過程需要使用歸納法,在此就不進行下去了。不過呢可以通過與自上而下的上濾演算法來進行簡單的對比:首先上濾建堆需要對所有的元素都執行上濾操作,而且越是接近堆底的元素可能需要交換的次數就越多,這意味這可能會有較多的元素需要進行較多次數的交換;而下濾建堆只需對所有內部結點進行下濾操作,這在操作的元素數量上就少了一半,其次越接近堆底的內部結點所需要的交換次數越少,這意味大多數的內部結點只需執行較少的交換,只有離堆頂越近的元素才需要較多的交換,而越接近堆頂,元素數量越少。由此可以看出自下而上的下濾建堆演算法在時間複雜度的漸進意義上來講要優於自上而下的上濾演算法。事實上自下而上的下來吧建堆演算法稱之為弗洛伊德建堆法

**如下

template

<

typename t>

void

heapify

(t *s, size_t size);if

((rank =

getlastinternal()

)<0)

//獲取最後乙個內部節點

return

;while

(rank>=0)

//對每乙個內部結點做下濾

}

堆(Heap)大根堆 小根堆

具有以下的特點 1 完全二叉樹 2 heap中儲存的值是偏序 min heap 父節點的值小於或等於子節點的值 max heap 父節點的值大於或等於子節點的值 一般都用陣列來表示堆,i結點的父結點下標就為 i 1 2。它的左右子結點下標分別為2 i 1和2 i 2。如第0個結點左右子結點下標分別為...

堆(Heap)大根堆 小根堆

目錄一般都用陣列來表示堆,i結點的父結點下標就為 i 1 2。它的左右子結點下標分別為2 i 1和2 i 2。如第0個結點左右子結點下標分別為1和2。插入乙個元素 新元素被加入到heap的末尾,然後更新樹以恢復堆的次序。每次插入都是將新資料放在陣列最後。可以發現從這個新資料的父結點到根結點必然為乙個...

堆排序(大根堆 小根堆)

堆 堆實際上是一棵完全二叉樹,其任何一非葉節點滿足性質 key i key 2i 1 key i key 2i 2 或者key i key 2i 1 key key 2i 2 即任何一非葉節點的關鍵字不大於或者不小於其左右孩子節點的關鍵字。堆分為大頂堆和小頂堆,滿足key i key 2i 1 ke...