16、Go优先队列

16、Go 优先队列


优先级队列是一种抽象数据类型,每个元素都有“优先级”,出队顺序由优先级决定,而非入队顺序。
常见策略。

  • 最大优先级队列:优先级最高的先出(大顶堆)
  • 最小优先级队列:优先级最低的先出(小顶堆)

Go 标准库的 container/heap 默认实现的是小顶堆(堆顶是最小元素),若要大顶堆只需反转 Less 比较逻辑即可。

container/heap 的接口约定

任何类型只要实现了 heap.Interface 就能作为堆使用:

type Interface interface {
    sort.Interface          // Len, Less, Swap
    Push(x any)             // 在末尾追加元素
    Pop() any               // 从末尾移除元素
}

sort.Interface 需要:

  • Len() int
  • Less(i, j int) bool
  • Swap(i, j int)

需要额外实现 Push 和 Pop,但注意:它们的操作对象都是切片的末尾,而不是堆顶。heap.Push / heap.Pop 会自动维护堆的性质,调用 Push 时往末尾追加,然后上浮;Pop 时把堆顶交换到末尾,然后切掉末尾,再下沉。

基础实现示例

整数最小堆(小顶堆)

package main

import (
    "container/heap"
    "fmt"
)

type IntHeap []int

func (h IntHeap) Len() int           { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] } // 小顶堆
func (h IntHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }

func (h *IntHeap) Push(x any) {
    *h = append(*h, x.(int))
}

func (h *IntHeap) Pop() any {
    old := *h
    n := len(old)
    x := old[n-1]
    *h = old[:n-1]
    return x
}

func main() {
    h := &IntHeap{2, 1, 5}
    heap.Init(h)           // 建立堆结构

    heap.Push(h, 3)        // 添加元素
    fmt.Println("min:", (*h)[0]) // 查看堆顶: 1

    for h.Len() > 0 {
        fmt.Print(heap.Pop(h), " ") // 1 2 3 5
    }
}

自定义结构的优先级队列(任务调度)

type Task struct {
    Priority int
    Name     string
}

type TaskQueue []*Task

func (tq TaskQueue) Len() int { return len(tq) }
func (tq TaskQueue) Less(i, j int) bool {
    return tq[i].Priority < tq[j].Priority // 小顶堆,数值小优先级高
}
func (tq TaskQueue) Swap(i, j int) { tq[i], tq[j] = tq[j], tq[i] }

func (tq *TaskQueue) Push(x any) {
    *tq = append(*tq, x.(*Task))
}

func (tq *TaskQueue) Pop() any {
    old := *tq
    n := len(old)
    item := old[n-1]
    old[n-1] = nil // 防止内存泄漏
    *tq = old[:n-1]
    return item
}

// 使用示例
func main() {
    tq := &TaskQueue{}
    heap.Init(tq)
    heap.Push(tq, &Task{3, "low"})
    heap.Push(tq, &Task{1, "high"})
    heap.Push(tq, &Task{2, "mid"})

    for tq.Len() > 0 {
        task := heap.Pop(tq).(*Task)
        fmt.Printf("%s(%d) ", task.Name, task.Priority)
        // 输出: high(1) mid(2) low(3)
    }
}

获得大顶堆:修改 Less 为 return tq[i].Priority > tq[j].Priority,或将数值取反、改用 After 时间等。

heap 包的内部机制

heap.Init 从最后一个非叶子节点开始,对每个元素执行下沉(down)操作,时间 O(n)。
heap.Push 把元素追加到末尾,然后执行上浮(up),时间 O(log n)。
heap.Pop 把堆顶和末尾元素交换,移除末尾(原堆顶),并对新堆顶下沉,时间 O(log n)。
heap.Remove 将指定索引元素与末尾交换后移除,再根据情况上浮或下沉,时间 O(log n)。
heap.Fix 在某个元素优先级改变后,重新调整位置,时间 O(log n)。

常见注意事项:

  1. 类型安全:Push 参数是 any,需做类型断言;建议封装类型安全方法。
  2. 内存泄漏:Pop 移除元素时,若切片存储指针,需把被删位置置 nil,避免引用无法被 GC 回收。
  3. 并发不安全:container/heap 不是并发安全的。多协程操作需自行加锁,或使用 channel 构建并发安全的优先队列。
  4. 性能敏感:如果元素数量极大且频繁修改,可考虑自己实现针对特定类型的二叉堆,避免接口开销和类型断言。
操作 时间复杂度
Init O(n)
Push O(log n)
Pop O(log n)
Remove O(log n)
Fix O(log n)

内存开销:底层是切片,动态扩容策略和普通切片一致(Go 的容量倍增机制)。接口调用会带来少量额外开销,但通常可接受。

并发安全的优先级队列

最常见的模式是 sync.Mutex 包装:

type SafePQ struct {
    mu sync.Mutex
    pq TaskQueue
}

func (s *SafePQ) Push(task *Task) {
    s.mu.Lock()
    defer s.mu.Unlock()
    heap.Push(&s.pq, task)
}

func (s *SafePQ) Pop() *Task {
    s.mu.Lock()
    defer s.mu.Unlock()
    if s.pq.Len() == 0 {
        return nil
    }
    return heap.Pop(&s.pq).(*Task)
}

更复杂的场景可用 sync.Cond 实现阻塞等待(当队列为空时等待新元素),或借助 semaphore 模式。

posted @ 2026-09-24 16:46  爱喝奶茶的CC怪  阅读(8)  评论(0)    收藏  举报