網絡上關於堆排序的算法博客多是給圖給真相(因爲翻了很多文章還是不清楚,堆排序的過程是什麼?所以自己來搞),今天我介紹一下我對堆排序的理解及實戰分析。
先介紹一下堆:它分最大堆和最小堆,二者的不同就是父節點和子節點的關係;最大堆要求父節點要比子節點大,而最小堆要求比父節點要比子節點小。其次是堆所依賴的數據結構,它本身是一顆完全二叉樹,由於完全二叉樹特性,父節點和左右子節點間位置的關係可通過公式得出(如果根節點位置從0開始,那麼左右節點和父節點位置的關係爲:nl = n*2+1, nr = n*2+2;同樣可以通過逆運算,由左右子節點位置信息獲取父節點的位置信息:n = (nl-1)/2, n = (nr-1)/2,`這裏依賴int類型做除法的時候結果會將小數點後面的數刪除,只保留整數部分`),所以數組便可成爲堆數據結構的不二之選(從父節點訪問堆子節點的時間複雜度爲0(1),而堆所佔的空間複雜度爲O(n),n爲堆的大小)。
然後介紹算法的核心過程:堆排序之前有一個堆的初始化過程,這個過程的目的是爲了得到一個"所有的父節點都大於其左右子節點"這樣的堆,堆初始化的過程是 "從第一個非葉子節點開始, 第一個父節點下沉,然後順序進行第二父節點的下沉,直到根節點下沉完畢" 這樣的過程。當然還有其它辦法進行堆的初始化,比如利用二叉樹的特性(遍歷左右子樹等),不過 "無論怎麼交換位置,二叉樹各個路徑上的節點保持一致,順序可以不一致"(原因,只有父子節點間位置的替換,並無兄弟節點間位置的替換),這是題外話。(細節會在算法中體現,比如左右子節點相等的情況怎麼處理)
這裏給出一個假設“真相”(算法是否正確也和這個假設是否成立相關):
【
在堆完成初始化後, 所有的父節點都將大於子節點(這一步爲後續操作的基礎);
堆的刪除:將根節點和堆的尾節點交換位置並且堆的大小-1; 此時堆的平衡(父節點大於其子節點)可能會被打破, 再讓新的根節點下沉,直至滿足該節點大於其子節點或者該節點成爲葉子節點時, 堆將重新維持到平衡狀態。
堆的新增:堆的大小+1,新增節點成爲堆末尾的葉子節點;此時堆的平衡(父節點大於其子節點)可能會被打破, 再讓末尾節點上浮,直至滿足其父節點大於該節點或該節點成爲根節點時,堆將重新維持到平衡狀態。
】
最後是排序:其實就是一個堆刪除的過程(把整個堆刪除完,排序也就完成了)。
實現如下(Go):
//堆排序實現
/**
算法核心:
1. 堆重構
[
(以最大堆舉例)
(增加)上浮: 與父節點比較, 如果大於父節點, 與父結點交換位置後(上浮節點繼續垂直上浮直到成爲根節點或者小於父節點);
(刪除)下沉: 與子節點比較, 如果小於子節點, 選擇較大子節點與之交換位置, 然後繼續下沉(直至其成爲葉子節點);
]
2. 排序: 將堆重構後的根節點與末尾節點置換, 根節點開始下沉, 直到末尾節點成爲根節點
原理:
在"堆的初始化"完成後, 所有的父節點都大於子節點(這一步是後續操作的基礎), 當根節點和堆的尾節點交換位置後(刪除操作),
堆的平衡(父節點大於子節點)會被打破了,可以通過新的根節點的持續下沉操作, 將堆重新維持到平衡狀態。
*/
package main
import "fmt"
//垂直上浮
func heapVerUp(heap []int, i int) {
if i < 0 {
return
}
for ; i >= 1; {
pi := (i-1)/2
if heap[i] > heap[pi] {
heap[i], heap[pi] = heap[pi], heap[i]
i = pi
continue
}
break
}
}
//堆的初始化
func heapInit(heap []int) {
//讓非葉子節點進行下沉
for i := (len(heap)-1)/2+1; i >= 0; i-- {
heapDown(heap, i, len(heap)-1)
}
}
//節點下沉
func heapDown(heap []int, i, hl int) {
if hl == 0 {
return
}
for {
nl := i*2+1
nr := i*2+2
if nl <= hl && nr <= hl{
//與左節點比較, 如果左右節點相等的情況, 左子節點優先進行
if heap[nl]>=heap[nr] && heap[i]<heap[nl] {
heap[nl], heap[i] = heap[i], heap[nl]
i = nl
continue
//與右節點比較
} else if heap[i]<heap[nr] {
heap[nr], heap[i] = heap[i], heap[nr]
i = nr
continue
}
} else if nl<=hl && heap[i]<heap[nl] {
//同末尾節點比較
heap[nl], heap[i] = heap[i], heap[nl]
}
break
}
}
func heapSort(heap []int) {
//設置根節點位置從0開始, 則左右節點與父結點位置的關係爲 n左 = n*2+1 n右 = n*2+2
li := len(heap)-1
if li == 0 {
return
}
//平移上浮
heapInit(heap)
fmt.Println(heap)
for ; li >= 1; li-- {
heap[li], heap[0] = heap[0], heap[li]
heapDown(heap, 0, li-1)
}
}
func main() {
//sli := []int {1, 2, 3, 4, 5, 6, 7, 8, 9} //OK
//sli := []int {9, 8, 7, 6, 5, 4, 3, 2, 1} //OK
//sli := []int {4, 3, 5, 6, 2, 8, 7, 9, 1} //OK
//sli := []int {2, 2, 1, 3, 4, 1, 2, 1, 2, 2} //OK
//sli := []int {1, 1, 1, 1, 1, 1, 2, 1, 1, 1} //OK
sli := []int {1, 44, 22, 11}
heapSort(sli)
fmt.Println(sli)
}
而堆新增節點的操作,可應用到優先級任務隊列。