golang 排序_堆 堆排序 优先队列 图文详解(Golang实现)
引入
在實際應(yīng)用中,我們經(jīng)常需要從一組對象中查找 最大值 或 最小值 。當(dāng)然我們可以每次都先排序,然后再進(jìn)行查找,但是這種做法效率很低。哪么有沒有一種特殊的數(shù)據(jù)結(jié)構(gòu),可以高效率的實現(xiàn)我們的需求呢,答案就是 堆(heap)
堆分為最小堆和最大堆,它們的性質(zhì)相似,我們以最小堆為例子。
最小堆
舉例
如上圖所示,就為一個最小堆。
特性
- 是一棵完全二叉樹
如果一顆二叉樹的任何結(jié)點,或者是樹葉,或者左右子樹均非空,則這棵二叉樹稱做滿二叉樹(full binary tree)
如果一顆二叉樹最多只有最下面的兩層結(jié)點度數(shù)可以小于2,并且最下面一層的結(jié)點都集中在該層最左邊的連續(xù)位置上,則此二叉樹稱做完全二叉樹(complete binary tree)
- 局部有序
最小堆對應(yīng)的完全二叉樹中所有結(jié)點的值均不大于其左右子結(jié)點的值,且一個結(jié)點與其兄弟之間沒有必然的聯(lián)系
二叉搜索樹中,左子 < 父 < 右子
存儲結(jié)構(gòu)
由于堆是一棵完全二叉樹,所以我們可以用順序結(jié)構(gòu)來存儲它,只需要計算簡單的代數(shù)表達(dá)式,就能夠非常方便的查找某個結(jié)點的父結(jié)點和子節(jié)點,既避免了使用指針來保持結(jié)構(gòu),又能高效的執(zhí)行相應(yīng)操作。
結(jié)點i的左子結(jié)點為2xi+1,右子結(jié)點為2xi+2結(jié)點i的父節(jié)點為(i-1)/2數(shù)據(jù)結(jié)構(gòu)
// 本例為最小堆// 最大堆只需要修改less函數(shù)即可type Heap []intfunc (h Heap) swap(i, j int) { h[i], h[j] = h[j], h[i]}func (h Heap) less(i, j int) bool { return h[i] < h[j]}如上所示,我們使用slice來存儲我們的數(shù)據(jù),為了后續(xù)方便我們在此定義了 swap 和 less 函數(shù),分別用來交換兩個結(jié)點和比較大小。
插入-Push
如上圖所示,首先,新添加的元素加入末尾。為了保持最小堆的性質(zhì),需要沿著其祖先的路徑, 自下而上 依次比較和交換該結(jié)點與父結(jié)點的位置,直到重新滿足堆的性質(zhì)為止。
這樣會出現(xiàn)兩種情況,要么新結(jié)點升到最小堆的頂端,要么到某一位置時發(fā)現(xiàn)父結(jié)點比新插入的結(jié)點關(guān)鍵值小。
上面的流程代碼如下:
func (h Heap) up(i int) { for { f := (i - 1) / 2 // 父親結(jié)點 if i == f || h.less(f, i) { break } h.swap(f, i) i = f }}實現(xiàn)了最核心的 up 操作后,我們的插入操作 push 便很簡單,代碼如下:
// 注意go中所有參數(shù)轉(zhuǎn)遞都是值傳遞// 所以要讓h的變化在函數(shù)外也起作用,此處得傳指針func (h *Heap) Push(x int) { *h = append(*h, x) h.up(len(*h) - 1)}刪除-Remove
如上圖所示,首先把最末端的結(jié)點填入要刪除節(jié)點的位置,然后刪除末端元素,同理,這樣做也可能導(dǎo)致破壞最小堆的堆序特性。
為了保持堆的特性,末端元素需要與被刪除位置的父結(jié)點做比較,如果小于父結(jié)點,就要up(詳細(xì)代碼看插入)如果大于父結(jié)點,就要再和被刪除位置的子結(jié)點做比較,即down,直到該結(jié)點下降到小于最小子結(jié)點為止。
上面down的流程代碼如下:
func (h Heap) down(i int) { for { l := 2*i + 1 // 左孩子 if l >= len(h) { break // i已經(jīng)是葉子結(jié)點了 } j := l if r := l + 1; r < len(h) && h.less(r, l) { j = r // 右孩子 } if h.less(i, j) { break // 如果父結(jié)點比孩子結(jié)點小,則不交換 } h.swap(i, j) // 交換父結(jié)點和子結(jié)點 i = j //繼續(xù)向下比較 }}實現(xiàn)了核心的 down 操作后,我們的 Remove 便很簡單,代碼如下:
// 刪除堆中位置為i的元素// 返回被刪元素的值func (h *Heap) Remove(i int) (int, bool) { if i < 0 || i > len(*h)-1 { return 0, false } n := len(*h) - 1 h.swap(i, n) // 用最后的元素值替換被刪除元素 // 刪除最后的元素 x := (*h)[n] *h = (*h)[0:n] // 如果當(dāng)前元素大于父結(jié)點,向下篩選 if (*h)[i] > (*h)[(i-1)/2] { h.down(i) } else { // 當(dāng)前元素小于父結(jié)點,向上篩選 h.up(i) } return x, true}彈出-Pop
當(dāng)i=0時, Remove 就是 Pop
// 彈出堆頂?shù)脑?#xff0c;并返回其值func (h *Heap) Pop() int { n := len(*h) - 1 h.swap(0, n) x := (*h)[n] *h = (*h)[0:n] h.down(0) return x}初始化-Init
在我們講完了堆的核心操作 up 和 down 后,我們來講如何根據(jù)一個數(shù)組構(gòu)造一個最小堆。
其實我們可以寫個循環(huán),然后將各個元素依次 push 進(jìn)去,但是這次我們利用數(shù)學(xué)規(guī)律,直接由一個數(shù)組構(gòu)造最小堆。
首先,將所有關(guān)鍵碼放到一維數(shù)組中,此時形成的完全二叉樹并不具備最小堆的特征,但是僅包含葉子結(jié)點的子樹已經(jīng)是堆。
即在有n個結(jié)點的完全二叉樹中,當(dāng) i>n/2-1 時,以i結(jié)點為根的子樹已經(jīng)是堆。
func (h Heap) Init() { n := len(h) // i > n/2-1 的結(jié)點為葉子結(jié)點本身已經(jīng)是堆了 for i := n/2 - 1; i >= 0; i-- { h.down(i) }}測試
func main() { var h = heap.Heap{20, 7, 3, 10, 15, 25, 30, 17, 19} h.Init() fmt.Println(h) // [3 7 20 10 15 25 30 17 19] h.Push(6) fmt.Println(h) // [3 6 20 10 7 25 30 17 19 15] x, ok := h.Remove(5) fmt.Println(x, ok, h) // 25 true [3 6 15 10 7 20 30 17 19] y, ok := h.Remove(1) fmt.Println(y, ok, h) // 6 true [3 7 15 10 19 20 30 17] z := h.Pop() fmt.Println(z, h) // 3 [7 10 15 17 19 20 30]}堆排序
在講完堆的基礎(chǔ)知識后,我們再來看堆排序就變得非常簡單。利用最小堆的特性,我們每次都從堆頂彈出一個元素(這個元素就是當(dāng)前堆中的最小值),即可實現(xiàn)升序排序。代碼如下:
// 堆排序var res []intfor len(h) != 0 { res = append(res, h.Pop())}fmt.Println(res)優(yōu)先隊列
優(yōu)先隊列是0個或者多個元素的集合,每個元素都有一個關(guān)鍵碼,執(zhí)行的操作有查找,插入和刪除等。
優(yōu)先隊列的主要特點是支持從一個集合中快速地查找并移出具有最大值或最小值的元素。
堆是一種很好的優(yōu)先隊列的實現(xiàn)方法。
參考資料
- 《數(shù)據(jù)結(jié)構(gòu)與算法》張銘 王騰蛟 趙海燕 編著
- GO SDK 1.13.1 /src/container/heap
最后
本文是自己的學(xué)習(xí)筆記,在刷了幾道LeetCode中關(guān)于堆的題目后,感覺應(yīng)該系統(tǒng)的學(xué)習(xí)和總結(jié)一下這一重要的數(shù)據(jù)結(jié)構(gòu)了。
總結(jié)
以上是生活随笔為你收集整理的golang 排序_堆 堆排序 优先队列 图文详解(Golang实现)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 平果手机桌面计算机,苹果手机便签记事本怎
- 下一篇: android高德marker添加点击,