由於 Golang 的標準庫中包含現成的heap包,所以網上大部分文章都是在寫如何使用這個heap包,不過堆排作為大廠的一個常見面試考點,是不會滿足於僅讓你用heap包去實現的,至少要做到能夠手搓一個簡易版堆排。
以下是來自力扣官網的友情提醒:
「堆排」在很多大公司的面試中都很常見,不瞭解的同學建議參考《算法導論》或者大家的數據結構教材,一定要學會這個知識點哦!^_^
一個簡易版的堆排,主要包含四個方法(方法名稱隨意):
- 插入-Push:用於向堆中插入數據
- 彈出-Shift:用於取出堆頂數據
- 上浮-Up:一般不對外,僅在堆內部使用,主要用於在插入數據後重構堆
- 下沉-Down:同上,一般僅在內部使用,主要用於在取出堆頂數據後重構堆
由於go標準包集成了小根堆,所以此處實現一個大根堆,如果想要小根堆,只需將上浮下沉中的判斷條件取反即可;
廢話不多説,直接上代碼:
// 大根堆結構:用一個隊列模擬二叉樹結構
type MaxHeap struct {
Que []int
}
// 插入數據:插到切片末尾,執行上浮操作
func (p *MaxHeap) Push(v int) {
p.Que = append(p.Que, v)
p.HeapUp(len(p.Que) - 1)
}
// 上浮邏輯:較簡單,就是不斷與parent節點值對比,大於parent,就交換雙方的值
func (p *MaxHeap) HeapUp(idx int) {
parentId := (idx - 1) / 2 //獲取parent節點下標:由於0下標是堆頂,所以是 (idx - 1) / 2
if parentId < 0 || p.Que[idx] <= p.Que[parentId] {
return
}
p.Que[idx], p.Que[parentId] = p.Que[parentId], p.Que[idx]
p.HeapUp(parentId)
}
// 取出堆頂數據:0下標即為堆頂;為了維持堆結構的正確性,取出後需與最後一個葉子節點交換數據並執行下沉操作
func (p *MaxHeap) Shift() int {
n := len(p.Que)
// 0下標代表堆頂:即堆中的最大或最小值
v := p.Que[0]
// 注意這裏:需要將隊列的最後一個下標與0下標調換值,再對0下標執行下沉操作;
// 由於是模擬二叉樹,所以隊列最後一個下標,即代表最後一個葉子節點,之所以與這個節點調換,是因為:
// 堆排是一棵【完全二叉樹】,最後一個葉子節點被移除後,不影響完全二叉樹結構,正好用來替換被移除的頭節點;
p.Que[0], p.Que[n-1] = p.Que[n-1], p.Que[0]
p.Que = p.Que[:n-1]
p.HeapDown(0)
return v
}
// 下沉操作:不斷與自己的左右孩子節點對比,並與其中的較大值作調換
func (p *MaxHeap) HeapDown(idx int) {
n := len(p.Que)
// 獲取左右孩子下標:堆排中的隊列下標其實就是模擬二叉樹的層序遍歷順序;
// Root節點是0下標,依次類推,左右孩子座標不難推導出來;
L, R := idx*2+1, idx*2+2
if n == 0 || L >= n {
return // 越界判斷
}
peakId := idx // 峯值下標,即子堆的堆頂
// 分別與左右孩子對比,選擇其中的較大值作為子堆的堆頂
if p.Que[peakId] < p.Que[L] {
peakId = L
}
if R < n && p.Que[peakId] < p.Que[R] {
peakId = R
}
if peakId != idx {
// 如果左右孩子的值大於自身,則調換值,繼續下沉
p.Que[idx], p.Que[peakId] = p.Que[peakId], p.Que[idx]
p.HeapDown(peakId)
}
}
堆排實踐:
這裏選取力扣【215. 數組中的第K個最大元素】作為測試題目:
func findKthLargest(nums []int, k int) int {
mHeap := MaxHeap{}
for _, v := range nums {
mHeap.Push(v) // 建立堆
}
for i := 0; i < k-1; i++ {
mHeap.Shift() // 執行 k-1 次堆頂取出操作,則此時的堆頂即為答案
}
return mHeap.Que[0]
}
func main() {
arr := []int{5, 2, 4, 1, 3, 6, 0}
fmt.Println(findKthLargest(arr, 4))
}
// 輸出:3
時間複雜度:O(nlogn),建堆的時間代價是 O(n),刪除的總代價是 O(klogn),因為 k<n,故漸進時間複雜為 O(n+klogn)=O(nlogn)。
空間複雜度:O(logn),即遞歸使用棧空間的空間代價。
順帶吐槽一句:力扣的部分題解,純粹是為了炫技寫的,可讀性真的不敢恭維。