大家好,我是藍胖子,我一直相信程式設計是一門實踐性的技術,其中演算法也不例外,初學者可能往往對它可望而不可及,覺得很難,學了又忘,忘其實是由於沒有真正搞懂演算法的應用場景,所以我準備出一個系列,囊括我們在日常開發中常用的演算法,並結合實際的應用場景,真正的感受演算法的魅力。
今天我們就來看看堆這種資料結構。
原始碼已經上傳到github
https://github.com/HobbyBear/codelearning/tree/master/heap
原理
在詳細介紹堆之前,先來看一種場景,很多時候我們並不需要對所有元素進行排序,而只需要取其中前topN的元素,這樣的情況如果按效能較好的排序演算法,比如歸併或者快排需要n*log( n)的時間複雜度,n為資料總量,排好序後取出前N條資料,而如果用堆這種資料結構則可以在n*log(N)的時間複雜度內找到這N條資料,N的資料量遠遠小於資料總量n。
接著我們來看看堆的定義和性質,堆是一種樹狀結構,且分為最小堆和最大堆,最大堆的性質有父節點大於左右子節點,最小堆的性質則是父節點小於左右子節點。如下圖所示:
並且堆是一顆完全二叉樹,完全二叉樹的定義如下:
若設二叉樹的深度為h,除第 h 層外,其它各層 (1~h-1) 的結點數都達到最大個數,第 h 層所有的結點都連續集中在最左邊,這就是完全二叉樹。
因為結點都集中在左側,所以我們可以從上到下,從左到右對堆中節點進行標號,如下圖所示:
從0開始對堆中節點進行標號後,可以得到以下規律:
父節點標號 = (子節點標號-1)/2
左節點標號 = 父節點標號 *2 + 1
右節點標號 = 父節點標號 *2 + 2
有了標號和父子節點的標號間的關係,我們可以用一個陣列來儲存堆這種資料結構,下面以構建一個最大堆為例,介紹兩種構建堆的方式。
HeapInsert
heapInsert的方式是從零開始,逐個往堆中插入陣列中的元素,並不斷調整新的節點,讓新節點的父節點滿足最大堆父節點大於其子節點的性質,這個調整的過程被稱作ShiftUp。當陣列中元素全部插入完成時,就構建了一個最大堆。程式碼如下:
func HeapInsert(arr []int) *Heap {
h := &Heap{arr: make([]int, 0, len(arr))}
for _, num := range arr {
h.Insert(num)
}
return h
}
Heapify
heapify的方式是假設陣列已經是一個完全二叉樹了,然後找到樹中的最後一個非葉子節點,然後透過比較它與其子節點的大小關係,讓其滿足最大堆的父節點大於其子節點的性質,這樣的操作被稱作ShifDown,對每個非葉子節點都執行ShifDown操作,直至根節點,這樣就達到了將一個普通陣列變成一個堆的目的。
如果堆的長度是n,那麼最後一個非葉子節點是 n/2 -1 ,所以可以寫出如下邏輯,
func Heapify(arr []int) *Heap {
h := &Heap{arr: arr}
lastNotLeaf := len(arr)/ 2 -1
for i:= lastNotLeaf;i >= 0; i-- {
h.ShiftDown(i)
}
return h
}
取出根節點
取出根節點的邏輯比較容易,將根節點結果儲存,之後讓它與堆中最後一個節點交換位置,然後從索引0開始進行ShiftDown操作,就又能讓整個陣列變成一個堆了。
func (h *Heap) Pop() int {
num := h.arr[0]
swap(h.arr, 0, len(h.arr)-1)
h.arr = h.arr[:len(h.arr)-1]
h.ShiftDown(0)
return num
}
ShiftUp,ShiftDown實現
下面我將shiftUp和shiftDown的原始碼展示出來,它們都是一個遞迴操作,因為在每次shiftUp或者shiftDown成功後,其父節點或者子節點還要繼續執行shifUp或shiftDown操作。
// 從標號為index的節點開始做shifUp操作
func (h *Heap) ShiftUp(index int) {
if index == 0 {
return
}
parent := (index - 1) / 2
if h.arr[parent] < h.arr[index] {
swap(h.arr, parent, index)
h.ShiftUp(parent)
}
}
// 從標號為index的節點開始做shifDown操作
func (h *Heap) ShiftDown(index int) {
left := index*2 + 1
right := index*2 + 2
if left < len(h.arr) && right < len(h.arr) {
if h.arr[left] >= h.arr[right] && h.arr[left] > h.arr[index] {
swap(h.arr, left, index)
h.ShiftDown(left)
}
if h.arr[right] > h.arr[left] && h.arr[right] > h.arr[index] {
swap(h.arr, right, index)
h.ShiftDown(right)
}
}
if left >= len(h.arr) {
return
}
if right >= len(h.arr) {
if h.arr[left] > h.arr[index] {
swap(h.arr, left, index)
h.ShiftDown(left)
}
}
}