堆的原理以及實現O(lgn)

藍胖子的程式設計夢發表於2023-09-27

大家好,我是藍胖子,我一直相信程式設計是一門實踐性的技術,其中演算法也不例外,初學者可能往往對它可望而不可及,覺得很難,學了又忘,忘其實是由於沒有真正搞懂演算法的應用場景,所以我準備出一個系列,囊括我們在日常開發中常用的演算法,並結合實際的應用場景,真正的感受演算法的魅力。

今天我們就來看看堆這種資料結構。

原始碼已經上傳到github

https://github.com/HobbyBear/codelearning/tree/master/heap

原理

在詳細介紹堆之前,先來看一種場景,很多時候我們並不需要對所有元素進行排序,而只需要取其中前topN的元素,這樣的情況如果按效能較好的排序演算法,比如歸併或者快排需要n*log( n)的時間複雜度,n為資料總量,排好序後取出前N條資料,而如果用堆這種資料結構則可以在n*log(N)的時間複雜度內找到這N條資料,N的資料量遠遠小於資料總量n。

接著我們來看看堆的定義和性質,堆是一種樹狀結構,且分為最小堆和最大堆,最大堆的性質有父節點大於左右子節點,最小堆的性質則是父節點小於左右子節點。如下圖所示:

image.png

並且堆是一顆完全二叉樹,完全二叉樹的定義如下:

若設二叉樹的深度為h,除第 h 層外,其它各層 (1~h-1) 的結點數都達到最大個數,第 h 層所有的結點都連續集中在最左邊,這就是完全二叉樹。

因為結點都集中在左側,所以我們可以從上到下,從左到右對堆中節點進行標號,如下圖所示:

image.png

從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)  
      }  
   }  
}

相關文章