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() intLess(i, j int) boolSwap(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)。
常见注意事项:
- 类型安全:
Push参数是any,需做类型断言;建议封装类型安全方法。 - 内存泄漏:
Pop移除元素时,若切片存储指针,需把被删位置置nil,避免引用无法被 GC 回收。 - 并发不安全:
container/heap不是并发安全的。多协程操作需自行加锁,或使用 channel 构建并发安全的优先队列。 - 性能敏感:如果元素数量极大且频繁修改,可考虑自己实现针对特定类型的二叉堆,避免接口开销和类型断言。
| 操作 | 时间复杂度 |
|---|---|
| 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 模式。
浙公网安备 33010602011771号