堆排序總結
這是排序,不是查找!!!查找去找二叉排序樹等。
滿二叉樹一定是完全二叉樹,但完全二叉樹不一定是滿二叉樹。
構建頂堆:
a.構造初始堆
b.從最后一層非葉節點開始調整,一直到根節點
c.如果還不滿足,重復b操作,直到構建出一個大頂堆或小頂堆。
構建大頂堆:每次調整都是從父節點、左孩子節點、右孩子節點三者中選擇最大者跟父節點進行交換。
(重點)輸出排序后的序列:一個大頂堆,獲得最大值后,
1. 輸出堆頂;
2. 將最后一個堆元素送入堆頂,堆被破壞;
3. 重新構建一個堆,此時,從堆頂開始調整,即將父節點和孩子結點最大值與父節點的值交換,一直調整到最后一層非葉結點;
4. 重復1,2,3操作,直到將堆中所有元素輸出,則所有輸出序列就是降序排列。
下面的內容來源:https://www.jianshu.com/p/6b526aa481b1
堆就是用數組實現的二叉樹,所有它沒有使用父指針或者子指針。
堆根據“堆屬性”來排序,“堆屬性”決定了樹中節點的位置。
堆的常用方法:
- 構建優先隊列
- 支持堆排序
- 快速找出一個集合中的最小值(或者最大值)
- 在朋友面前裝逼
堆屬性
堆分為兩種:最大堆和最小堆,兩者的差別在於節點的排序方式。
在最大堆中,父節點的值比每一個子節點的值都要大;
在最小堆中,父節點的值比每一個子節點的值都要小。
這就是所謂的“堆屬性”,並且這個屬性對堆中的每一個節點都成立。
例子:

這是一個最大堆,因為每一個父節點的值都比其子節點要大。10
比 7
和 2
都大。7
比 5
和 1
都大。
根據這一屬性,那么最大堆總是將其中的最大值存放在樹的根節點。而對於最小堆,根節點中的元素總是樹中的最小值。
堆屬性非常的有用,因為堆常常被當做優先隊列使用,因為可以快速的訪問到“最重要”(優先級高)的元素。
注意:堆的根節點中存放的是最大或者最小元素,但是其他節點的排序順序是未知的。例如,在一個最大堆中,最大的那一個元素總是位於 index 0 的位置,但是最小的元素則未必是最后一個元素。--唯一能夠保證的是最小的元素是一個葉節點,但是不確定是哪一個。
堆和普通樹的區別
堆並不能取代二叉搜索樹,它們之間有相似之處也有一些不同。我們來看一下兩者的主要差別:
1. 節點的順序。
在二叉搜索樹中,左子節點必須比父節點小,右子節點必須必比父節點大。但是在堆中並非如此。
在最大堆中兩個子節點都必須比父節點小,而在最小堆中,它們都必須比父節點大。
2. 內存占用。
普通樹占用的內存空間比它們存儲的數據要多。你必須為節點對象以及左/右子節點指針分配額外的內存。
堆僅僅使用一個數組來存儲數據,且不使用指針。
3. 平衡。
二叉搜索樹必須是“平衡”的情況下,其大部分操作的復雜度才能達到O(log n)。
你可以按任意順序位置插入/刪除數據,或者使用 AVL 樹或者紅黑樹,但是在堆中實際上不需要整棵樹都是有序的。我們只需要滿足"堆屬性"即可,所以在堆中平衡不是問題。因為堆中數據的組織方式可以保證O(log n) 的性能。
4. 搜索。
在二叉樹中搜索會很快,但是在堆中搜索會很慢,堆根本不是用來搜索的。在堆中搜索不是第一優先級,因為使用堆的目的是將最大(或者最小)的節點放在最前面,從而快速的進行相關插入、刪除操作。
來自數組的樹
用數組來實現樹相關的數據結構也許看起來有點古怪,但是它在時間和空間山都是很高效的。
我們准備將上面的例子中的樹這樣存儲:
[ 10, 7, 2, 5, 1 ]
就這多!我們除了一個簡單的數組以外,不需要任何額外的空間。
如果我們不允許使用指針,那么我們怎么知道哪一個節點是父節點,哪一個節點是它的子節點呢?問得好!節點在數組中的位置index 和它的父節點以及子節點的索引之間有一個映射關系。
(重點)如果 i
是節點的索引,那么下面的公式就給出了它的父節點和子節點在數組中的位置:
parent(i) = floor((i - 1)/2) left(i) = 2i + 1 right(i) = 2i + 2
注意 right(i)
就是簡單的 left(i) + 1
。左右節點總是處於相鄰的位置。
我們將寫公式放到前面的例子中驗證一下。
Node | Array index (i ) |
Parent index | Left child | Right child |
---|---|---|---|---|
10 | 0 | -1 | 1 | 2 |
7 | 1 | 0 | 3 | 4 |
2 | 2 | 0 | 5 | 6 |
5 | 3 | 1 | 7 | 8 |
1 | 4 | 1 | 9 | 10 |
注意:根節點
(10)
沒有父節點,因為-1
不是一個有效的數組索引。同樣,節點(2)
,(5)
和(1)
沒有子節點,因為這些索引已經超過了數組的大小,所以我們在使用這些索引值的時候需要保證是有效的索引值。
復習一下,在最大堆中,父節點的值總是要大於(或者等於)其子節點的值。這意味下面的公式對數組中任意一個索引 i
都成立:
array[parent(i)] >= array[i]
可以用上面的例子來驗證一下這個堆屬性。
如你所見,這些公式允許我們不使用指針就可以找到任何一個節點的父節點或者子節點。事情比簡單的去掉指針要復雜,但這就是時間換空間:我們節約了空間,但是要進行更多計算。幸好這些計算很快並且只需要O(1)的時間。
理解數組索引和節點位置之間的關系非常重要。這里有一個更大的堆,它有15個節點被分成了4層:

圖片中的數字不是節點的值,而是存儲這個節點的數組索引!這里是數組索引和樹的層級之間的關系:

由上圖可以看到,數組中父節點總是在子節點的前面。
注意這個方案有一些限制。你可以在普通二叉樹中按照下面的方式組織數據,但是在堆中不可以:

在堆中,在當前層級所有的節點都已經填滿之前不允許開是下一層的填充(即完全二叉樹),所以堆總是有這樣的形狀:
注意:你可以使用普通樹來模擬堆,但是那對空間是極大的浪費。
小測驗,假設我們有這樣一個數組:
[ 10, 14, 25, 33, 81, 82, 99 ]
這是一個有效的堆嗎?答案是 yes !一個從低到高有序排列的數組是以有效的最小堆,我們可以將這個堆畫出來:
堆屬性適用於每一個節點,因為父節點總是比它的孩子節點小。(你也可以驗證一下:一個從高到低有序排列的數組是一個有效的最大堆)
注意:並不是每一個最小堆都是一個有序數組!要將堆轉換成有序數組,需要使用堆排序。
更多數學公式
如果你好奇,這里有更多的公式描述了堆的一些確定屬性。你不需要知道這些,但它們有時會派上用場。 可以直接跳過此部分!
樹的高度是指從樹的根節點到最低的葉節點所需要的步數,或者更正式的定義:高度是指節點之間的邊的最大值。
一個高度為 h 的堆有 h+1 層(和之前學過的數據結構書上定義的高度有點不一樣)。
下面這個堆的高度是3,所以它有4層:
如果一個堆有 n 個節點,那么它的高度是 h = floor(log2(n))。這是因為我們總是要將這一層完全填滿以后才會填充新的一層。上面的例子有 15 個節點,所以它的高度是 floor(log2(15)) = floor(3.91) = 3
。
如果最下面的一層已經填滿,那么那一層包含 2^h 個節點。樹中這一層以上所有的節點數目為 2^h - 1。同樣是上面這個例子,最下面的一層有8個節點,實際上就是 2^3 = 8
。前面的三層一共包含7的節點,即:2^3 - 1 = 8 - 1 = 7
。
所以整個堆中的節點數目為:* 2^(h+1) - 1*。上面的例子中,2^4 - 1 = 16 - 1 = 15
葉節點總是位於數組的 floor(n/2) 和 n-1 之間。
可以用堆做什么?
(重點)有兩個原始操作用於保證插入或刪除節點以后堆是一個有效的最大堆或者最小堆:
shiftUp()
: 如果一個節點比它的父節點大(最大堆)或者小(最小堆),那么需要將它同父節點交換位置。這樣是這個節點在數組的位置上升。shiftDown()
: 如果一個節點比它的子節點小(最大堆)或者大(最小堆),那么需要將它向下移動。這個操作也稱作“堆化(heapify)”。
shiftUp 或者 shiftDown 是一個遞歸的過程,所以它的時間復雜度是 O(log n)。
基於這兩個原始操作還有一些其他的操作:
insert(value)
: 在堆的尾部添加一個新的元素,然后使用shiftUp
來修復對。remove()
: 移除並返回最大值(最大堆)或者最小值(最小堆)。為了將這個節點刪除后的空位填補上,需要將最后一個元素移到根節點的位置,然后使用shiftDown
方法來修復堆。removeAtIndex(index)
: 和remove()
一樣,差別在於可以移除堆中任意節點,而不僅僅是根節點。當它與子節點比較位置不時無序時使用shiftDown()
,如果與父節點比較發現無序則使用shiftUp()
。replace(index, value)
:將一個更小的值(最小堆)或者更大的值(最大堆)賦值給一個節點。由於這個操作破壞了堆屬性,所以需要使用shiftUp()
來修復堆屬性。
上面所有的操作的時間復雜度都是 O(log n),因為 shiftUp 和 shiftDown 都很費時。還有少數一些操作需要更多的時間:
search(value)
:堆不是為快速搜索而建立的,但是replace()
和removeAtIndex()
操作需要找到節點在數組中的index,所以你需要先找到這個index。時間復雜度:O(n)。buildHeap(array)
:通過反復調用insert()
方法將一個(無序)數組轉換成一個堆。如果你足夠聰明,你可以在 O(n) 時間內完成。- 堆排序:由於堆就是一個數組,我們可以使用它獨特的屬性將數組從低到高排序。時間復雜度:O(n lg n)。
堆還有一個 peek()
方法,不用刪除節點就返回最大值(最大堆)或者最小值(最小堆)。時間復雜度 O(1) 。
注意:到目前為止,堆的常用操作還是使用
insert()
插入一個新的元素,和通過remove()輸出根節點
。兩者的時間復雜度都是O(log n)。其其他的操作是用於支持更高級的應用,比如說建立一個優先隊列。
插入(重點)
我們通過一個插入例子來看看插入操作的細節。我們將數字 16
插入到這個堆中:
堆的數組是: [ 10, 7, 2, 5, 1 ]
第一股是將新的元素插入到數組的尾部。數組變成:
[ 10, 7, 2, 5, 1, 16 ]
相應的樹變成了:
16
被添加最后一行的第一個空位。
不行的是,現在堆屬性不滿足,因為 2
在 16
的上面,我們需要將大的數字在上面(這是一個最大堆)
為了恢復堆屬性,我們需要交換 16
和 2
。
現在還沒有完成,因為 10
也比 16
小。我們繼續交換我們的插入元素和它的父節點,直到它的父節點比它大或者我們到達樹的頂部。這就是所謂的 shift-up,每一次插入操作后都需要進行。它將一個太大或者太小的數字“浮起”到樹的頂部。
最后我們得到的堆:
現在每一個父節點都比它的子節點大。
刪除根節點(重點)
我們將這個樹中的 (10)
刪除:

現在頂部有一個空的節點,怎么處理?

(重點) 當插入節點的時候,我們將新的值返給數組的尾部。現在我們來做相反的事情:我們取出數組中的最后一個元素,將它放到樹的頂部,然后再修復堆屬性。
(1)
。為了保持最大堆的堆屬性,我們需要樹的頂部是最大的數據。現在有兩個數字可用於交換 7
和 2
。我們選擇這兩者中的較大者稱為最大值放在樹的頂部,所以交換 7
和 1
,現在樹變成了:

繼續調整堆直到該節點沒有任何子節點或者它比兩個子節點都要大為止。對於我們的堆,我們只需要再有一次交換就恢復了堆屬性:
刪除任意節點
絕大多數時候你需要刪除的是堆的根節點,因為這就是堆的設計用途。
但是,刪除任意節點也很有用。這是 remove()
的通用版本,它可能會使用到 shiftDown
和 shiftUp
。
我們還是用前面的例子,刪除 (7)
:
對應的數組是
[ 10, 7, 2, 5, 1 ]
你知道,移除一個元素會破壞最大堆或者最小堆屬性。我們需要將刪除的元素和最后一個元素交換:
[ 10, 1, 2, 5, 7 ]
最后一個元素就是我們需要返回的元素;然后調用 removeLast()
來將它刪除。 (1)
比它的子節點小,所以需要 shiftDown()
來修復。
然而,shift down 不是我們要處理的唯一情況。也有可能我們需要 shift up。考慮一下從下面的堆中刪除 (5)
會發生什么:
現在 (5)
和 (8)
交換了。因為 (8)
比它的父節點大,我們需要 shiftUp()
。