Go語(yǔ)言利用heap實(shí)現(xiàn)優(yōu)先級(jí)隊(duì)列
1. heap使用
在go語(yǔ)言的標(biāo)準(zhǔn)庫(kù)container中,實(shí)現(xiàn)了三中數(shù)據(jù)類型:heap,list,ring,list在前面一篇文章中已經(jīng)寫(xiě)了,現(xiàn)在要寫(xiě)的是heap(堆)的源碼剖析。
首先,學(xué)會(huì)怎么使用heap,第一步當(dāng)然是導(dǎo)入包了,代碼如下:
package main import ( "container/heap" "fmt" )
這個(gè)堆使用的數(shù)據(jù)結(jié)構(gòu)是最小二叉樹(shù),即根節(jié)點(diǎn)比左邊子樹(shù)和右邊子樹(shù)的所有值都小。源碼里面只是實(shí)現(xiàn)了一個(gè)接口,它的定義如下:
type Interface interface { sort.Interface Push(x interface{}) // add x as element Len() Pop() interface{} // remove and return element Len() - 1. }
從這個(gè)接口可以看出,其繼承了sort.Interface接口,那么sort.Interface的定義是什么呢?源碼如下:
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) }
也就是說(shuō),我們要使用go標(biāo)準(zhǔn)庫(kù)給我們提供的heap,那么必須自己實(shí)現(xiàn)這些接口定義的方法,需要實(shí)現(xiàn)的方法如下:
- Len() int
- Less(i, j int) bool
- Swap(i, j int)
- Push(x interface{})
- Pop() interface{}
實(shí)現(xiàn)了這五個(gè)方法的數(shù)據(jù)類型才能使用go標(biāo)準(zhǔn)庫(kù)給我們提供的heap,下面簡(jiǎn)單示例為定義一個(gè)IntHeap類型,并實(shí)現(xiàn)上面五個(gè)方法。
type IntHeap []int // 定義一個(gè)類型 func (h IntHeap) Len() int { return len(h) } // 綁定len方法,返回長(zhǎng)度 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方法,交換兩個(gè)元素位置 h[i], h[j] = h[j], h[i] } func (h *IntHeap) Pop() interface{} { // 綁定pop方法,從最后拿出一個(gè)元素并返回 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)) }
針對(duì)IntHeap實(shí)現(xiàn)了這五個(gè)方法之后,我們就可以使用heap了,下面是具體使用方法:
func main() { h := &IntHeap{2, 1, 5, 6, 4, 3, 7, 9, 8, 0} // 創(chuàng)建slice heap.Init(h) // 初始化heap fmt.Println(*h) fmt.Println(heap.Pop(h)) // 調(diào)用pop heap.Push(h, 6) // 調(diào)用push fmt.Println(*h) for len(*h) > 0 { fmt.Printf("%d ", heap.Pop(h)) } }
輸出結(jié)果:
[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的使用了。
2. heap提供的方法
heap提供的方法不多,具體如下:
h := &IntHeap{3, 8, 6} // 創(chuàng)建IntHeap類型的原始數(shù)據(jù) func Init(h Interface) // 對(duì)heap進(jìn)行初始化,生成小根堆(或大根堆) func Push(h Interface, x interface{}) // 往堆里面插入內(nèi)容 func Pop(h Interface) interface{} // 從堆頂pop出內(nèi)容 func Remove(h Interface, i int) interface{} // 從指定位置刪除數(shù)據(jù),并返回刪除的數(shù)據(jù) func Fix(h Interface, i int) // 從i位置數(shù)據(jù)發(fā)生改變后,對(duì)堆再平衡,優(yōu)先級(jí)隊(duì)列使用到了該方法
3. heap源碼剖析
heap的內(nèi)部實(shí)現(xiàn),是使用最小(最大)堆,索引排序從根節(jié)點(diǎn)開(kāi)始,然后左子樹(shù),右子樹(shù)的順序方式。 內(nèi)部實(shí)現(xiàn)的down和up分別表示對(duì)堆中的某個(gè)元素向下保證最小(最大)堆和向上保證最小(最大)堆。
當(dāng)往堆中插入一個(gè)元素的時(shí)候,這個(gè)元素插入到最右子樹(shù)的最后一個(gè)節(jié)點(diǎn)中,然后調(diào)用up向上保證最小(最大)堆。
當(dāng)要從堆中推出一個(gè)元素的時(shí)候,先吧這個(gè)元素和右子樹(shù)最后一個(gè)節(jié)點(diǎn)交換,然后彈出最后一個(gè)節(jié)點(diǎn),然后對(duì)root調(diào)用down,向下保證最小(最大)堆。
好了,開(kāi)始分析源碼:
首先,在使用堆之前,必須調(diào)用它的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() // 獲取數(shù)據(jù)的長(zhǎng)度 for i := n/2 - 1; i >= 0; i-- { // 從長(zhǎng)度的一半開(kāi)始,一直到第0個(gè)數(shù)據(jù),每個(gè)位置都調(diào)用down方法,down方法實(shí)現(xiàn)的功能是保證從該位置往下保證形成堆 down(h, i, n) } }
接下來(lái)看down的源碼:
func down(h Interface, i0, n int) bool { i := i0 // 中間變量,第一次存儲(chǔ)的是需要保證往下需要形成堆的節(jié)點(diǎn)位置 for { // 死循環(huán) j1 := 2*i + 1 // i節(jié)點(diǎn)的左子孩子 if j1 >= n || j1 < 0 { // j1 < 0 after int overflow // 保證其左子孩子沒(méi)有越界 break } j := j1 // left child // 中間變量j先賦值為左子孩子,之后j將被賦值為左右子孩子中最?。ù螅┑囊粋€(gè)孩子的位置 if j2 := j1 + 1; j2 < n && !h.Less(j1, j2) { j = j2 // = 2*i + 2 // right child } // 這之后,j被賦值為兩個(gè)孩子中的最?。ù螅┖⒆拥奈恢茫ㄗ钚』蜃畲笥蒐ess中定義的決定) if !h.Less(j, i) { break } // 若j大于(小于)i,則終止循環(huán) h.Swap(i, j) // 否則交換i和j位置的值 i = j // 令i=j,繼續(xù)循環(huán),保證j位置的子數(shù)是堆結(jié)構(gòu) } return i > i0 }
這是建立堆的核心代碼,其實(shí),down并不能完全保證從某個(gè)節(jié)點(diǎn)往下每個(gè)節(jié)點(diǎn)都能保持堆的特性,只能保證某個(gè)節(jié)點(diǎn)的值如果不滿足堆的性質(zhì),則將該值與其孩子交換,直到該值放到適合的位置,保證該值及其兩個(gè)子孩子滿足堆的性質(zhì)。
但是,如果是通過(guò)Init循環(huán)調(diào)用down將能保證初始化后所有的節(jié)點(diǎn)都保持堆的特性,這是因?yàn)檠h(huán)開(kāi)始的i := n/2 - 1
的取值位置,將會(huì)取到最大的一個(gè)擁有孩子節(jié)點(diǎn)的節(jié)點(diǎn),并且該節(jié)點(diǎn)最多只有兩個(gè)孩子,并且其孩子節(jié)點(diǎn)是葉子節(jié)點(diǎn),從該節(jié)點(diǎn)往前每個(gè)節(jié)點(diǎn)如果都能保證down的特性,則整個(gè)列表也就符合了堆的性質(zhì)了。
同樣,有down就有up,up保證的是某個(gè)節(jié)點(diǎn)如果向上沒(méi)有保證堆的性質(zhì),則將其與父節(jié)點(diǎn)進(jìn)行交換,直到該節(jié)點(diǎn)放到某個(gè)特定位置保證了堆的性質(zhì)。代碼如下:
func up(h Interface, j int) { for { // 死循環(huán) i := (j - 1) / 2 // parent // j節(jié)點(diǎn)的父節(jié)點(diǎn) if i == j || !h.Less(j, i) { // 如果越界,或者滿足堆的條件,則結(jié)束循環(huán) break } h.Swap(i, j) // 否則將該節(jié)點(diǎn)和父節(jié)點(diǎn)交換 j = i // 對(duì)父節(jié)點(diǎn)繼續(xù)進(jìn)行檢查直到根節(jié)點(diǎn) } }
以上兩個(gè)方法就是最核心的方法了,所有暴露出來(lái)的方法無(wú)非就是對(duì)這兩個(gè)方法進(jìn)行的封裝。我們來(lái)看看以下這些方法的源碼:
func Push(h Interface, x interface{}) { h.Push(x) // 將新插入進(jìn)來(lái)的節(jié)點(diǎn)放到最后 up(h, h.Len()-1) // 確保新插進(jìn)來(lái)的節(jié)點(diǎn)網(wǎng)上能保證堆結(jié)構(gòu) } func Pop(h Interface) interface{} { n := h.Len() - 1 // 把最后一個(gè)節(jié)點(diǎn)和第一個(gè)節(jié)點(diǎn)進(jìn)行交換,之后,從根節(jié)點(diǎn)開(kāi)始重新保證堆結(jié)構(gòu),最后把最后那個(gè)節(jié)點(diǎn)數(shù)據(jù)丟出并返回 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位置的節(jié)點(diǎn)和最后一個(gè)節(jié)點(diǎn)進(jìn)行交換,之后保證從i節(jié)點(diǎn)往下及往上都保證堆結(jié)構(gòu),最后把最后一個(gè)節(jié)點(diǎn)的數(shù)據(jù)丟出并返回 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節(jié)點(diǎn)的數(shù)值發(fā)生改變后,需要保證堆的再平衡,先調(diào)用down保證該節(jié)點(diǎn)下面的堆結(jié)構(gòu),如果有位置交換,則需要保證該節(jié)點(diǎn)往上的堆結(jié)構(gòu),否則就不需要往上保證堆結(jié)構(gòu),一個(gè)小小的優(yōu)化 up(h, i) } }
以上就是go里面的heap所有的源碼了,我也就不貼出完整版源碼了,以上理解全部基于個(gè)人的理解,如有不當(dāng)之處,還望批評(píng)指正。
4. 利用heap實(shí)現(xiàn)優(yōu)先級(jí)隊(duì)列
既然用到了heap,那就用heap實(shí)現(xiàn)一個(gè)優(yōu)先級(jí)隊(duì)列吧,這個(gè)功能是很好的一個(gè)功能。
源碼如下:
package main import ( "container/heap" "fmt" ) type Item struct { value string // 優(yōu)先級(jí)隊(duì)列中的數(shù)據(jù),可以是任意類型,這里使用string priority int // 優(yōu)先級(jí)隊(duì)列中節(jié)點(diǎn)的優(yōu)先級(jí) index int // index是該節(jié)點(diǎn)在堆中的位置 } // 優(yōu)先級(jí)隊(duì)列需要實(shí)現(xiàn)heap的interface type PriorityQueue []*Item // 綁定Len方法 func (pq PriorityQueue) Len() int { return len(pq) } // 綁定Less方法,這里用的是小于號(hào),生成的是小根堆 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是為了標(biāo)識(shí)該數(shù)據(jù)已經(jīng)出了優(yōu)先級(jí)隊(duì)列了 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) } // 更新修改了優(yōu)先級(jí)和值的item在優(yōu)先級(jí)隊(duì)列中的位置 func (pq *PriorityQueue) update(item *Item, value string, priority int) { item.value = value item.priority = priority heap.Fix(pq, item.index) } func main() { // 創(chuàng)建節(jié)點(diǎn)并設(shè)計(jì)他們的優(yōu)先級(jí) items := map[string]int{"二毛": 5, "張三": 3, "狗蛋": 9} i := 0 pq := make(PriorityQueue, len(items)) // 創(chuàng)建優(yōu)先級(jí)隊(duì)列,并初始化 for k, v := range items { // 將節(jié)點(diǎn)放到優(yōu)先級(jí)隊(duì)列中 pq[i] = &Item{ value: k, priority: v, index: i} i++ } heap.Init(&pq) // 初始化堆 item := &Item{ // 創(chuàng)建一個(gè)item value: "李四", priority: 1, } heap.Push(&pq, item) // 入優(yōu)先級(jí)隊(duì)列 pq.update(item, item.value, 6) // 更新item的優(yōu)先級(jí) for len(pq) > 0 { item := heap.Pop(&pq).(*Item) fmt.Printf("%.2d:%s index:%.2d\n", item.priority, item.value, item.index) } }
輸出結(jié)果:
03:張三 index:-01
05:二毛 index:-01
06:李四 index:-01
09:狗蛋 index:-01
到此這篇關(guān)于Go語(yǔ)言利用heap實(shí)現(xiàn)優(yōu)先級(jí)隊(duì)列的文章就介紹到這了,更多相關(guān)Go語(yǔ)言 heap內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang開(kāi)發(fā)動(dòng)態(tài)庫(kù)的實(shí)現(xiàn)
這篇文章主要介紹了Golang開(kāi)發(fā)動(dòng)態(tài)庫(kù)的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11Go語(yǔ)言實(shí)現(xiàn)JSON解析的方法詳解
在日常項(xiàng)目中,使用Json格式進(jìn)行數(shù)據(jù)封裝是比較常見(jiàn)的操作。本文將詳細(xì)講解如何利用Go語(yǔ)言實(shí)現(xiàn)JSON的解析,感興趣的小伙伴可以學(xué)習(xí)一下2022-04-04golang 對(duì)私有函數(shù)進(jìn)行單元測(cè)試的實(shí)例
這篇文章主要介紹了golang 對(duì)私有函數(shù)進(jìn)行單元測(cè)試的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-05-05golang?中?channel?的詳細(xì)使用、使用注意事項(xiàng)及死鎖問(wèn)題解析
這篇文章主要介紹了golang?中?channel?的詳細(xì)使用、使用注意事項(xiàng)及死鎖分析,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03