堆的使用
堆的使用
关于堆和优先队列,标准库已经实现了核心部分,详见container/heap 基本自定义集合(一般为一个切片)实现 heap.Interface 的 5 个函数,就可以用了。 详见标准库两个 example 开头的测试文件。
需求
假设我们要同时用到大顶堆和小顶堆,怎么办?简单起见,假设元素都是int。
初步实现
参考标准库example_intheap_test.go,很容易写出以下代码:
type MinHeap []int
type MaxHeap []int
func (h MinHeap) Len() int { return len(h) }
func (h MinHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h MinHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *MinHeap) Push(x interface{}) { *h = append(*h, x.(int)) }
func (h *MinHeap) Pop() interface{} {
x := (*h)[len(*h)-1]
*h = (*h)[:len(*h)-1]
return x
}
func (h MaxHeap) Len() int { return len(h) }
func (h MaxHeap) Less(i, j int) bool { return h[i] > h[j] }
func (h MaxHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *MaxHeap) Push(x interface{}) { *h = append(*h, x.(int)) }
func (h *MaxHeap) Pop() interface{} {
x := (*h)[len(*h)-1]
*h = (*h)[:len(*h)-1]
return x
}改进实现
重复代码很多,大顶堆和小顶堆只有比较逻辑不同,即那个 Less 函数,其他实现没有差别。
有没有办法减少代码呢?
还真想到一个,在 Go 里,函数是一等公民,可以当一般变量传递使用,我们不妨给自定义 Heap 增加一个属性 cmp,类型是 func(int, int) bool,也就是上边的 Less 函数的类型
type Cmp func(int, int) bool
type Heap struct {
slice []int
cmp Cmp
}
// implement heap.Interface
func (h *Heap) Len() int { return len(h.slice) }
func (h *Heap) Less(i, j int) bool { return h.cmp(h.slice[i], h.slice[j]) }
func (h *Heap) Swap(i, j int) { h.slice[i], h.slice[j] = h.slice[j], h.slice[i] }
func (h *Heap) Push(x interface{}) { h.slice = append(h.slice, x.(int)) }
func (h *Heap) Pop() interface{} {
x := h.slice[h.Len()-1]
h.slice = h.slice[:h.Len()-1]
return x
}代码一下子减少一半!使用时是这样:
minHeap := &Heap{}
minHeap.cmp = func(a, b int) bool {
return a < b
}
maxHeap := &Heap{}
maxHeap.cmp = func(a, b int) bool {
return a > b
}要注意几点: 首先在刚刚初始化完要立即赋予其 cmp,cmp 为 nil的话后边程序会崩溃给我们看~ 其次 cmp 后续不能被修改,不然堆的逻辑会混乱
基本解决了问题,虽然不够完美~
应用
- 数据流、滑动窗口的中位数
用两个堆来解决中位数的问题,非常妙,尤其可以看看滑动窗口那个问题的解法,实现了一个 remove、push 和 pop 都是对数级复杂度的堆。
更进一步
可以重构标准库的 heap 包,参考 https://github.com/zrcoder/dsGo/tree/master/heap