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