博客 / 詳情

返回

go算法與數據結構:實現大小根堆、堆排

由於 Golang 的標準庫中包含現成的heap包,所以網上大部分文章都是在寫如何使用這個heap包,不過堆排作為大廠的一個常見面試考點,是不會滿足於僅讓你用heap包去實現的,至少要做到能夠手搓一個簡易版堆排。
以下是來自力扣官網的友情提醒:
「堆排」在很多大公司的面試中都很常見,不瞭解的同學建議參考《算法導論》或者大家的數據結構教材,一定要學會這個知識點哦!^_^

一個簡易版的堆排,主要包含四個方法(方法名稱隨意):

  • 插入-Push:用於向堆中插入數據
  • 彈出-Shift:用於取出堆頂數據
  • 上浮-Up:一般不對外,僅在堆內部使用,主要用於在插入數據後重構堆
  • 下沉-Down:同上,一般僅在內部使用,主要用於在取出堆頂數據後重構堆

由於go標準包集成了小根堆,所以此處實現一個大根堆,如果想要小根堆,只需將上浮下沉中的判斷條件取反即可;
廢話不多説,直接上代碼:

// 大根堆結構:用一個隊列模擬二叉樹結構
type MaxHeap struct {
    Que []int
}
// 插入數據:插到切片末尾,執行上浮操作
func (p *MaxHeap) Push(v int) {
    p.Que = append(p.Que, v)
    p.HeapUp(len(p.Que) - 1)
}
// 上浮邏輯:較簡單,就是不斷與parent節點值對比,大於parent,就交換雙方的值
func (p *MaxHeap) HeapUp(idx int) {
    parentId := (idx - 1) / 2 //獲取parent節點下標:由於0下標是堆頂,所以是 (idx - 1) / 2
    if parentId < 0 || p.Que[idx] <= p.Que[parentId] {
        return
    }
    p.Que[idx], p.Que[parentId] = p.Que[parentId], p.Que[idx]
    p.HeapUp(parentId)
}
// 取出堆頂數據:0下標即為堆頂;為了維持堆結構的正確性,取出後需與最後一個葉子節點交換數據並執行下沉操作
func (p *MaxHeap) Shift() int {
    n := len(p.Que)
    // 0下標代表堆頂:即堆中的最大或最小值
    v := p.Que[0]
    // 注意這裏:需要將隊列的最後一個下標與0下標調換值,再對0下標執行下沉操作;
    // 由於是模擬二叉樹,所以隊列最後一個下標,即代表最後一個葉子節點,之所以與這個節點調換,是因為:
    // 堆排是一棵【完全二叉樹】,最後一個葉子節點被移除後,不影響完全二叉樹結構,正好用來替換被移除的頭節點;
    p.Que[0], p.Que[n-1] = p.Que[n-1], p.Que[0]
    p.Que = p.Que[:n-1]
    p.HeapDown(0)
    return v
}
// 下沉操作:不斷與自己的左右孩子節點對比,並與其中的較大值作調換
func (p *MaxHeap) HeapDown(idx int) {
    n := len(p.Que)
    // 獲取左右孩子下標:堆排中的隊列下標其實就是模擬二叉樹的層序遍歷順序;
    // Root節點是0下標,依次類推,左右孩子座標不難推導出來;
    L, R := idx*2+1, idx*2+2
    if n == 0 || L >= n {
        return // 越界判斷
    }
    peakId := idx // 峯值下標,即子堆的堆頂
    // 分別與左右孩子對比,選擇其中的較大值作為子堆的堆頂
    if p.Que[peakId] < p.Que[L] {
        peakId = L
    }
    if R < n && p.Que[peakId] < p.Que[R] {
        peakId = R
    }
    if peakId != idx {
        // 如果左右孩子的值大於自身,則調換值,繼續下沉
        p.Que[idx], p.Que[peakId] = p.Que[peakId], p.Que[idx]
        p.HeapDown(peakId)
    }
}

堆排實踐:
這裏選取力扣【215. 數組中的第K個最大元素】作為測試題目:

func findKthLargest(nums []int, k int) int {
    mHeap := MaxHeap{}
    for _, v := range nums {
        mHeap.Push(v) // 建立堆
    }
    for i := 0; i < k-1; i++ {
        mHeap.Shift() // 執行 k-1 次堆頂取出操作,則此時的堆頂即為答案
    }
    return mHeap.Que[0]
}
func main() {
    arr := []int{5, 2, 4, 1, 3, 6, 0}
    fmt.Println(findKthLargest(arr, 4))
}
// 輸出:3

時間複雜度:O(nlogn),建堆的時間代價是 O(n),刪除的總代價是 O(klogn),因為 k<n,故漸進時間複雜為 O(n+klogn)=O(nlogn)。
空間複雜度:O(logn),即遞歸使用棧空間的空間代價。


順帶吐槽一句:力扣的部分題解,純粹是為了炫技寫的,可讀性真的不敢恭維。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.