GO語言heap剖析及利用heap實現優先級隊列

GO語言heap剖析

本節內容

  1. heap使用
  2. heap提供的方法
  3. heap源碼剖析
  4. 利用heap實現優先級隊列

1. heap使用

在go語言的標準庫container中,實現了三中數據類型:heap,list,ring,list在前面一篇文章中已經寫了,如今要寫的是heap(堆)的源碼剖析。api

首先,學會怎麼使用heap,第一步固然是導入包了,代碼以下:數據結構

package main

import (
    "container/heap"
    "fmt"
)

這個堆使用的數據結構是最小二叉樹,即根節點比左邊子樹和右邊子樹的全部值都小。源碼裏面只是實現了一個接口,它的定義以下:app

type Interface interface {
    sort.Interface
    Push(x interface{}) // add x as element Len()
    Pop() interface{}   // remove and return element Len() - 1.
}

從這個接口能夠看出,其繼承了sort.Interface接口,那麼sort.Interface的定義是什麼呢?源碼以下:less

type Interface interface {
    // Len is the number of elements in the collection.
    Len() int
    // Less reports whether the element with
    // index i should sort before the element with index j.
    Less(i, j int) bool
    // Swap swaps the elements with indexes i and j.
    Swap(i, j int)
}

也就是說,咱們要使用go標準庫給咱們提供的heap,那麼必須本身實現這些接口定義的方法,須要實現的方法以下:ide

  • Len() int
  • Less(i, j int) bool
  • Swap(i, j int)
  • Push(x interface{})
  • Pop() interface{}

實現了這五個方法的數據類型才能使用go標準庫給咱們提供的heap,下面簡單示例爲定義一個IntHeap類型,並實現上面五個方法。優化

type IntHeap []int  // 定義一個類型

func (h IntHeap) Len() int { return len(h) }  // 綁定len方法,返回長度
func (h IntHeap) Less(i, j int) bool {  // 綁定less方法
    return h[i] < h[j]  // 若是h[i]<h[j]生成的就是小根堆,若是h[i]>h[j]生成的就是大根堆
}
func (h IntHeap) Swap(i, j int) {  // 綁定swap方法,交換兩個元素位置
    h[i], h[j] = h[j], h[i]
}

func (h *IntHeap) Pop() interface{} {  // 綁定pop方法,從最後拿出一個元素並返回
    old := *h
    n := len(old)
    x := old[n-1]
    *h = old[0 : n-1]
    return x
}

func (h *IntHeap) Push(x interface{}) {  // 綁定push方法,插入新元素
    *h = append(*h, x.(int))
}

針對IntHeap實現了這五個方法以後,咱們就可使用heap了,下面是具體使用方法:設計

func main() {
    h := &IntHeap{2, 1, 5, 6, 4, 3, 7, 9, 8, 0}  // 建立slice
    heap.Init(h)  // 初始化heap
    fmt.Println(*h)
    fmt.Println(heap.Pop(h))  // 調用pop
    heap.Push(h, 6)  // 調用push
    fmt.Println(*h)
    for len(*h) > 0 {
        fmt.Printf("%d ", heap.Pop(h))
    }

}
輸出結果:
[0 1 3 6 2 5 7 9 8 4]
0
[1 2 3 6 4 5 7 9 8 6]
1 2 3 4 5 6 6 7 8 9

上面就是heap的使用了。code

2. heap提供的方法

heap提供的方法很少,具體以下:排序

h := &IntHeap{3, 8, 6}  // 建立IntHeap類型的原始數據
func Init(h Interface)  // 對heap進行初始化,生成小根堆(或大根堆)
func Push(h Interface, x interface{})  // 往堆裏面插入內容
func Pop(h Interface) interface{}  // 從堆頂pop出內容
func Remove(h Interface, i int) interface{}  // 從指定位置刪除數據,並返回刪除的數據
func Fix(h Interface, i int)  // 從i位置數據發生改變後,對堆再平衡,優先級隊列使用到了該方法

3. heap源碼剖析

heap的內部實現,是使用最小(最大)堆,索引排序從根節點開始,而後左子樹,右子樹的順序方式。 內部實現的down和up分別表示對堆中的某個元素向下保證最小(最大)堆和向上保證最小(最大)堆。繼承

當往堆中插入一個元素的時候,這個元素插入到最右子樹的最後一個節點中,而後調用up向上保證最小(最大)堆。

當要從堆中推出一個元素的時候,先吧這個元素和右子樹最後一個節點交換,而後彈出最後一個節點,而後對root調用down,向下保證最小(最大)堆。

好了,開始分析源碼:

首先,在使用堆以前,必須調用它的Init方法,初始化堆,生成小根(大根)堆。Init方法源碼以下:

// A heap must be initialized before any of the heap operations
// can be used. Init is idempotent with respect to the heap invariants
// and may be called whenever the heap invariants may have been invalidated.
// Its complexity is O(n) where n = h.Len().
//
func Init(h Interface) {
    // heapify
    n := h.Len()  // 獲取數據的長度
    for i := n/2 - 1; i >= 0; i-- {  // 從長度的一半開始,一直到第0個數據,每一個位置都調用down方法,down方法實現的功能是保證從該位置往下保證造成堆
        down(h, i, n)
    }
}

接下來看down的源碼:

func down(h Interface, i0, n int) bool {
    i := i0  // 中間變量,第一次存儲的是須要保證往下須要造成堆的節點位置
    for {  // 死循環
        j1 := 2*i + 1  // i節點的左子孩子
        if j1 >= n || j1 < 0 { // j1 < 0 after int overflow  // 保證其左子孩子沒有越界
            break
        }
        j := j1 // left child  // 中間變量j先賦值爲左子孩子,以後j將被賦值爲左右子孩子中最小(大)的一個孩子的位置
        if j2 := j1 + 1; j2 < n && !h.Less(j1, j2) {
            j = j2 // = 2*i + 2  // right child
        }  // 這以後,j被賦值爲兩個孩子中的最小(大)孩子的位置(最小或最大由Less中定義的決定)
        if !h.Less(j, i) {
            break
        }  // 若j大於(小於)i,則終止循環
        h.Swap(i, j)  // 不然交換i和j位置的值
        i = j  // 令i=j,繼續循環,保證j位置的子數是堆結構
    }
    return i > i0
}

這是創建堆的核心代碼,其實,down並不能徹底保證從某個節點往下每一個節點都能保持堆的特性,只能保證某個節點的值若是不知足堆的性質,則將該值與其孩子交換,直到該值放到適合的位置,保證該值及其兩個子孩子知足堆的性質。

可是,若是是經過Init循環調用down將能保證初始化後全部的節點都保持堆的特性,這是由於循環開始的i := n/2 - 1的取值位置,將會取到最大的一個擁有孩子節點的節點,而且該節點最多隻有兩個孩子,而且其孩子節點是葉子節點,從該節點往前每一個節點若是都能保證down的特性,則整個列表也就符合了堆的性質了。

一樣,有down就有up,up保證的是某個節點若是向上沒有保證堆的性質,則將其與父節點進行交換,直到該節點放到某個特定位置保證了堆的性質。代碼以下:

func up(h Interface, j int) {
    for {  // 死循環
        i := (j - 1) / 2 // parent  // j節點的父節點
        if i == j || !h.Less(j, i) {  // 若是越界,或者知足堆的條件,則結束循環
            break
        }
        h.Swap(i, j)  // 不然將該節點和父節點交換
        j = i  // 對父節點繼續進行檢查直到根節點
    }
}

以上兩個方法就是最核心的方法了,全部暴露出來的方法無非就是對這兩個方法進行的封裝。咱們來看看如下這些方法的源碼:

func Push(h Interface, x interface{}) {
    h.Push(x)  // 將新插入進來的節點放到最後
    up(h, h.Len()-1)  // 確保新插進來的節點網上能保證堆結構
}

func Pop(h Interface) interface{} {
    n := h.Len() - 1  // 把最後一個節點和第一個節點進行交換,以後,從根節點開始從新保證堆結構,最後把最後那個節點數據丟出並返回
    h.Swap(0, n)
    down(h, 0, n)
    return h.Pop()
}

func Remove(h Interface, i int) interface{} {
    n := h.Len() - 1  pop只是remove的特殊狀況,remove是把i位置的節點和最後一個節點進行交換,以後保證從i節點往下及往上都保證堆結構,最後把最後一個節點的數據丟出並返回
    if n != i {
        h.Swap(i, n)
        down(h, i, n)
        up(h, i)
    }
    return h.Pop()
}

func Fix(h Interface, i int) {
    if !down(h, i, h.Len()) {  // i節點的數值發生改變後,須要保證堆的再平衡,先調用down保證該節點下面的堆結構,若是有位置交換,則須要保證該節點往上的堆結構,不然就不須要往上保證堆結構,一個小小的優化
        up(h, i)
    }
}

以上就是go裏面的heap全部的源碼了,我也就不貼出完整版源碼了,以上理解所有基於我的的理解,若有不當之處,還望批評指正。

4. 利用heap實現優先級隊列

既然用到了heap,那就用heap實現一個優先級隊列吧,這個功能是很好的一個功能。
源碼以下:

package main

import (
    "container/heap"
    "fmt"
)

type Item struct {
    value    string // 優先級隊列中的數據,能夠是任意類型,這裏使用string
    priority int    // 優先級隊列中節點的優先級
    index    int    // index是該節點在堆中的位置
}

// 優先級隊列須要實現heap的interface
type PriorityQueue []*Item

// 綁定Len方法
func (pq PriorityQueue) Len() int {
    return len(pq)
}

// 綁定Less方法,這裏用的是小於號,生成的是小根堆
func (pq PriorityQueue) Less(i, j int) bool {
    return pq[i].priority < pq[j].priority
}

// 綁定swap方法
func (pq PriorityQueue) Swap(i, j int) {
    pq[i], pq[j] = pq[j], pq[i]
    pq[i].index, pq[j].index = i, j
}

// 綁定put方法,將index置爲-1是爲了標識該數據已經出了優先級隊列了
func (pq *PriorityQueue) Pop() interface{} {
    old := *pq
    n := len(old)
    item := old[n-1]
    *pq = old[0 : n-1]
    item.index = -1
    return item
}

// 綁定push方法
func (pq *PriorityQueue) Push(x interface{}) {
    n := len(*pq)
    item := x.(*Item)
    item.index = n
    *pq = append(*pq, item)
}

// 更新修改了優先級和值的item在優先級隊列中的位置
func (pq *PriorityQueue) update(item *Item, value string, priority int) {
    item.value = value
    item.priority = priority
    heap.Fix(pq, item.index)
}

func main() {
    // 建立節點並設計他們的優先級
    items := map[string]int{"二毛": 5, "張三": 3, "狗蛋": 9}
    i := 0
    pq := make(PriorityQueue, len(items)) // 建立優先級隊列,並初始化
    for k, v := range items {             // 將節點放到優先級隊列中
        pq[i] = &Item{
            value:    k,
            priority: v,
            index:    i}
        i++
    }
    heap.Init(&pq) // 初始化堆
    item := &Item{ // 建立一個item
        value:    "李四",
        priority: 1,
    }
    heap.Push(&pq, item)           // 入優先級隊列
    pq.update(item, item.value, 6) // 更新item的優先級
    for len(pq) > 0 {
        item := heap.Pop(&pq).(*Item)
        fmt.Printf("%.2d:%s index:%.2d\n", item.priority, item.value, item.index)
    }
}

輸出結果:
03:張三 index:-01
05:二毛 index:-01
06:李四 index:-01
09:狗蛋 index:-01
相關文章
相關標籤/搜索