这篇文章将站在多种语言视角阐述堆的设计
对于阐述go的泛型
"go啥都好,就是没有泛型(^-^)"
对于go选手而言,go的堆是一个硬伤,在go众多简单的概念中,go的堆设计似乎格格不入,本处将以一个内置数据结构的设计阐述设计理由与使用方式.
先来看看常见的语言的堆用法(Python设计没啥代表性,因为python是解释性语言,它的类型一般运行时确定,而不需要泛型这种东西)
C++
std::priority_queue<int>a;
(go党一次暴击)
Java
var a=new PriorityQueue<Integer>();
(go党二次暴击)
C#
var a = new PriorityQueue<int, int>();
var b = new SortedSet<int>();
var c = new SortedDictionary<int, int>();
var d = new SortedList<int,int>();(Go党:嗯???怎么平衡树都这么多)
PHP
$a=new SplMaxHeap();
$b=new SplMinHeap();(Go党:"连PHP的都这么优雅吗?")
var a1=new java.util.PriorityQueue[Int]()
var a2=new java.util.TreeMap[Int,Int]()
var a3=new java.util.TreeSet[Int]()
var b1=scala.collection.mutable.PriorityQueue[Int]()
var b2=scala.collection.mutable.TreeMap[Int,Int]()
var b3=scala.collection.mutable.TreeSet[Int]()
var b4=scala.collection.mutable.SortedSet[Int]()
var b5=scala.collection.mutable.SortedMap[Int,Int]()(Go党:"你在逗我吗???")
请问有没有哪个语言没有堆,有:Dart
(好开心,但是它有"SpalyTree",~???)
好叭,其实很多语言来说都是有堆的一个"易用级别实现",为啥诸如现代新型语言:rust、kotlin这些都能拥有如此好用的堆实现而go却每次需要玩家手动实现接口呢,这离不开一个概念:"泛型",对于go来说,早期没有泛型,导致抽象力度不够出现的梗都挺多的:"你知道吗?隔壁的go还要手写max函数呢"?????
先说说泛型的作用,我们以传统的静态编程语言Java为例:

对于一个max函数来说很显然的是我们可以看到,对于每一种类型都实现了一种max.

我们可以很容易的看出,这两个max函数代码是一样的,只是类型不一样
C++

我们可以很清楚看到,C++则是借助了模板来实现了一个max泛型函数.这个函数使得很多类型(实现了比较运算符)可以直接调用max函数
相信这边大部分小伙伴看出来了吧,泛型常常我们用字母T表示,这个T可以是任意类型,也可以是有一些限制的类型,比如实现了某些接口的类。当正儿八经调用运行时,这个T才会被确定是哪一个类型,而这样做的好处就是"复用性",更准确地来讲,抽象力度更强了
对于Java而言,泛型是作为编译时检查的一项内容,当编译以后确定好了类型以后,就会擦除泛型T变化为实际类型.这样一来其实我们可以把原本需要填int、long之类的目标统一用T代替,而这样一来,我们可以把它们归一化为一个函数.所以常常来说我们会发现Java的优先队列和C++的都需要加一个<类型>,用来表示实际的类型用于替代泛型


可以看到这些泛型不仅抽象力度好,还受到了类型限制(毕竟不是啥玩意都可以用来做比较的,咱得实现比较器接口是吧,或者有比较函数,重载运算符是吧)
(咱来瞧瞧Go的)


可以看到,哇呜,一共要实现五个接口函数!!!!!!
为啥要实现这五个函数呢:
有幸手写过堆的朋友:
洛谷堆模板题
可以发现


官方堆中最重要的两个函数是用的了这三个函数,而在push和pop中用到了

这里的Push和Pop的实现和你写的实现是不一样的=>解释如下:
对于java、go一类的语言都有:实现接口就能干事的说法,而以这个堆为例:
你需要"先实现接口"再使用函数.接口中的push和pop的实现只是针对你实现它的类而言,
并不是堆本身的push和pop,通过源码我们可以看到heap.Push(你所实现接口的结构体实例,元素)最后会调"你所实现接口的实例的Push"

对于golang不得不提提一个特别的example

官方是写了两个例子IntHeap和一个普通的优先队列的例子("方便copy")
// This example demonstrates an integer heap built using the heap interface.
package heap_test
import (
"container/heap"
"fmt"
)
// An IntHeap is a min-heap of ints.
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) {
// Push and Pop use pointer receivers because they modify the slice's length,
// not just its contents.
*h = append(*h, x.(int))
}
func (h *IntHeap) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}
// This example inserts several ints into an IntHeap, checks the minimum,
// and removes them in order of priority.
func Example_intHeap() {
h := &IntHeap{2, 1, 5}
heap.Init(h)
heap.Push(h, 3)
fmt.Printf("minimum: %d\n", (*h)[0])
for h.Len() > 0 {
fmt.Printf("%d ", heap.Pop(h))
}
// Output:
// minimum: 1
// 1 2 3 5
}读官方的IntHeap实现就会发觉,行吧非得手搓轮子是吧>>>



你闲逛于需要实现的接口所在的包,忽然发现了"哇,金色传说!!!"
有三个常用的切片已经为你实现好了三个接口函数,你只需要实现Push和Pop
在此之前,我们有必要过一遍golang的一些必知必会
func (this A)Run(){
xxxxx
}
func (this *A)Run(){
xxxxx
}(*this).字段通常来说结构体指针先解指针再拿字段,如果这个结构体嵌套了不少层这玩意,岂不是要打好多好多好多"括号和解指针",所以有个非常常见的语法糖this->字段应运而生.呼~美观多了.帮助自动解指针,golang也有个非常非常非常"不错的语法糖",那就是无论接收器是结构体还是结构体指针,无论是当前变量为结构体变量还是结构体指针变量,咱都一个语法"this.字段",自动解指针.
type A struct {
}
func (curr *A) Run() {
}
func test() {
t1 := A{}
t2 := &A{}
(*t2).Run()
t2.Run()
t1.Run()
}开始正式解析这五个接口函数的作用
Len():该函数用于获取序列的长度,当然常用的就是切片序列了
Swap(i,j int):该函数的参数为序列的下标,交换两个数,那么很显然的是,利用golang的a,b=b,a轻松完成.提一嘴功能此函数在堆代码中主要是交换两个节点完成上下沉浮,有兴趣的可以在洛谷堆模板中学习手写堆,其实很短滴
Less(i,j int)bool:参数依旧是序列的两个下标,灰常灰常灰常经典的Cmp函数.相信不少的C++小伙伴会用到手写Cmp,Java小伙伴会用到实现比较器接口,使用匿名内部类,Python小伙伴则是使用lambda表达式的key函数sort(key=lambda:一个优先级),Less函数功能正是如此手写Cmp
Push(x any),any其实就是咱最神乎其神的"空接口"了,在golang当作空接口就是一个"Bug"存在,万物即我,我即万物.(等等,这不就是泛型T吗,Java的Object等基类的效果吗?手动删掉.any是当golang来到1.18版本以后终于推出的"泛型"(终于有了,咱站起来了)的一个特别的东西.其实,其实,其实看看源码吧:
type any = interface{}纳尼?就这吗?别说,空接口名字挺长的,any也不错...对于转换空接口,噢不,any而言成为某个类型我们常常用类型断言(跟强制转化恰不多)
var a=b.(转换的类型)还是有点高大上滴!!!,b是any类型.Push作用就是-------难以启齿,其实就是append(手动删去
官方的IntHeap的Push实现:
func (h *IntHeap) Push(x any) {
// Push and Pop use pointer receivers because they modify the slice's length,
// not just its contents.
*h = append(*h, x.(int))
}搞一个Push的名字让别人以为和堆的Push有啥关联(^v^)
Pop()any:等等,这么说来,Pop就是RmoveLast???(咱高大上点,那名字谁都看出来啥意思,Pop多智慧呀,切~),官方实现:
func (h *IntHeap) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}这么说来???看到这的你可能会说就这,我原来被糊弄到了???
呼呼呼,咱继续往下说.
既然如此,那么我们的关注点来到了接收器类型,和实现接口的实现"结构体",好拗口啊,还是喜欢Java的实现类这个名字.
type IntHeap []int???官方实现???屏幕前的你缓缓打出问好,啊就这吗???不是结构体吗?
(这么喜欢结构体?拍桌,拍桌,拍桌,那就手写一个吧)
结构体应当由两部分,一部分实现嵌套接口的三个函数,一部分实现Push和Pop
type Heap struct {
ans []int
}
func (h *Heap) Len() int {
}
func (h *Heap) Less(i, j int) bool {
}
func (h *Heap) Swap(i, j int) {
}
func (h *Heap) Push(x any) {
}
func (h *Heap) Pop()any{
}???听你这么一说,好像我自己都会了.easy(手动划去.
常常来说,对于初学者我们接收者全部设置为指针类型就行,对于"高手"(说的就是你们这些go的爱好者),当然就是根据理解的写出
type Heap struct {
ans []int
}
func (h Heap) Len() int {
}
func (h Heap) Less(i, j int) bool {
}
func (h Heap) Swap(i, j int) {
}
func (h *Heap) Push(x any) {
}
func (h *Heap) Pop() any {
}和官方代码如出一辙的代码.为啥前三个函数要用非指针呢.因为结构体的字段是指针(切片),所以外层的Swap操作切片的还是原切片.Soga!!!
那我们还是以全指针版本讲解吧:("当观众不是高手是吧",手动划去,防止一些朋友debug到疯)
func (h *Heap) Len() int {
return len(h.ans)
}func (h *Heap) Less(i, j int) bool {
return h.ans[i]<h.ans[j]
}func (h *Heap) Swap(i, j int) {
h.ans[i], h.ans[j] = h.ans[j], h.ans[i]
}func (h *Heap) Push(x any) {
h.ans = append(h.ans, x.(int))
}func (h *Heap) Pop() any {
old := h.ans
n := len(old)
top := old[n-1]
h.ans = old[0 : n-1]
return top
}package main
import (
"bufio"
"container/heap"
. "fmt"
"io"
"os"
"runtime/debug"
)
type Heap struct {
ans []int
}
func NewHeap() *Heap {
return &Heap{}
}
func (h *Heap) Len() int {
return len(h.ans)
}
func (h *Heap) Less(i, j int) bool {
return h.ans[i] < h.ans[j]
}
func (h *Heap) Swap(i, j int) {
h.ans[i], h.ans[j] = h.ans[j], h.ans[i]
}
func (h *Heap) Push(x any) {
h.ans = append(h.ans, x.(int))
}
func (h *Heap) Pop() any {
old := h.ans
n := len(old)
top := old[n-1]
h.ans = old[0 : n-1]
return top
}
func Solve(_r io.Reader, _w io.Writer) {
//in := bufio.NewReader(_r)
out := bufio.NewWriter(_w)
defer out.Flush()
//-----------------------------------------------------
var a = NewHeap() //这是一个IntMinHeap,更改Less即可变为最大堆
heap.Push(a, 12) //肿么这么像python
heap.Push(a, 87879)
heap.Push(a, -8989)
heap.Push(a, -889)
heap.Push(a, -89)
heap.Push(a, 97789) //随心所欲造数据
for a.Len() > 0 {
Fprintln(out, heap.Pop(a)) //呜~好像和python一样的
}
}
func main() {
debug.SetGCPercent(-1)
Solve(os.Stdin, os.Stdout)
}至此,你已经会堆了,而结构体元素版本,你也只需要更改Less的书写:例如常见的[]Pair,你只需要更改Less函数即可
(心想:又会了一个有b格的东西,真有b格,咱go玩家就是要不惧困难,手动划去)
type Heap struct {
ans sort.IntSlice
}
func NewHeap() *Heap {
return &Heap{}
}
func (h *Heap) Len() int {
return h.Len()
}
func (h *Heap) Less(i, j int) bool {
return h.Less(i,j)
}
func (h *Heap) Swap(i, j int) {
h.Swap(i,j)
}
func (h *Heap) Push(x any) {
h.ans = append(h.ans, x.(int))
}
func (h *Heap) Pop() any {
old := h.ans
n := len(old)
top := old[n-1]
h.ans = old[0 : n-1]
return top
}啊,这不是???反复调API,隔这搁那呢,还不如我自己写的(有用滴就不错了)
言归正传,在新一代的golang版本,由于增加了泛型参数,而golang的接收器对象有个明确的规定"无法使用原生本地对象".好费解的话,其实就是无法像kotlin一样扩展函数(噫噫噫,java和c++之类的不是也不行吗?它们不行,为啥我们不能行一下,手动划去).众所周知,编译器是很笨的(不对,很聪明的)

生气!!!!

生气++++

what???这么好糊弄
这里其实涉及到了~int,扩展int类型,有兴趣的可以看看
所以我们可以通过这样的方式,不仅可以调用原生的函数,还能写一些新的小玩意
Cmp包:
package cmp
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~string
}
func Less[T Ordered](x, y T) bool {
return (isNaN(x) && !isNaN(y)) || x < y
}
func Compare[T Ordered](x, y T) int {
xNaN := isNaN(x)
yNaN := isNaN(y)
if xNaN && yNaN {
return 0
}
if xNaN || x < y {
return -1
}
if yNaN || x > y {
return +1
}
return 0
}
func isNaN[T Ordered](x T) bool {
return x != x
}对于新版本的Golang而言,拥有了cmp.Ordered的泛型限定,凡是由这种泛型限定的,都可以直接调用<、>、==、!=这些运算符(这不就是默认运算符的实现吗???),不得不提一嘴,新版的Go泛型不要小尖尖,采用中括号[int],真不是看Scala的多跟着搞嘛(呜~)
所以说吗?整点活,来写一个通用结构体的泛型板子:
回忆下C++大哥,好像有点麻烦,好像咱go的泛型没那么厉害.看看Java大哥,比较器接口??这个简单,咱不叫Cmp了,多捞啊,叫Less函数
type Cmp[T any] interface {
Less(T) bool
}???为啥接口名叫Cmp(我说的是函数名嘿嘿)
那么这个很显然的是,这个接口有个比较函数,这个则是实际代码过程中需要实现的
type Heap[T Cmp[T]] struct {
ans []T
}当当当当,全新Heap闪亮登场.T类型是一个可比较类型即可.
剩下的???剩下的一个样,都看到这了!!!
func (curr *Heap[T]) Len() int {
return len(curr.ans)
}
func (curr *Heap[T]) Swap(x int, y int) {
curr.ans[x], curr.ans[y] = curr.ans[y], curr.ans[x]
}
func (curr *Heap[T]) Less(i, j int) bool {
return curr.ans[i].Less(curr.ans[j])
}
func (curr *Heap[T]) Push(x any) {
curr.ans = append(curr.ans, x.(T))
}
func (curr *Heap[T]) Pop() any {
old := curr.ans
n := len(old)
x := old[n-1]
curr.ans = old[0 : n-1]
return x
}
func NewHeap[T Cmp[T]]() *Heap[T] {
return &Heap[T]{}
}easy.来用一下Pair版本(咱这个也搞个泛型呗,当然不太熟的朋友,搞个最普通的int版本也是很简单的)
type Pair[T cmp.Ordered] struct {
x, y T
}
func NewPair[T cmp.Ordered](x T, y T) Pair[T] {
return Pair[T]{x, y}
}
func (curr Pair[T]) Less(other Pair[T]) bool {
if curr.x == other.x {
return curr.y < other.y
}
return curr.x < other.x
}豁???这不就是平常我们最常用的吗?没有任何学习压力啊,你不写这玩意,我们平常也会用到
PS:有的小伙伴会问(非go选手)为啥实现接口没有啥关键字implements这种.那是因为golang的解耦性很高,只要你实现了接口所有函数,就算实现了接口,很随意吧
呼呼,使用一下???
package main
import (
"bufio"
"container/heap"
"fmt"
"io"
"os"
"runtime/debug"
)
type Cmp[T any] interface {
Less(T) bool
}
type Heap[T Cmp[T]] struct {
ans []T
}
func (curr *Heap[T]) Len() int {
return len(curr.ans)
}
func (curr *Heap[T]) Swap(x int, y int) {
curr.ans[x], curr.ans[y] = curr.ans[y], curr.ans[x]
}
func (curr *Heap[T]) Less(i, j int) bool {
return curr.ans[i].Less(curr.ans[j])
}
func (curr *Heap[T]) Push(x any) {
curr.ans = append(curr.ans, x.(T))
}
func (curr *Heap[T]) Pop() any {
old := curr.ans
n := len(old)
x := old[n-1]
curr.ans = old[0 : n-1]
return x
}
func NewHeap[T Cmp[T]]() *Heap[T] {
return &Heap[T]{}
}
type Pair struct {
x, y int
}
func NewPair(x int, y int) Pair {
return Pair{x, y}
}
func (curr Pair) Less(other Pair) bool {
if curr.x == other.x {
return curr.y < other.y
}
return curr.x < other.x
}
func Solve(_r io.Reader, _w io.Writer) {
//in := bufio.NewReader(_r)
out := bufio.NewWriter(_w)
defer out.Flush()
//-----------------------------------------------------
var a = NewHeap[Pair]()
heap.Push(a, NewPair(154, 21))
heap.Push(a, NewPair(154, 31))
heap.Push(a, NewPair(28, 29))
heap.Push(a, NewPair(456, 2321))
heap.Push(a, NewPair(456, 232))
for a.Len() > 0 {
var a = heap.Pop(a).(Pair)
fmt.Println(a.x, a.y)
}
}
func main() {
debug.SetGCPercent(-1)
Solve(os.Stdin, os.Stdout)
}这个时候你只想大喊一声easy~
等等,那int咋办???
噢~忘了刚刚骗计算机的花招了?(其实也称不上骗~~)
type M int
func (curr M) Less(other M) bool {
return curr < other
}(好短~)
package main
import (
"bufio"
"cmp"
"container/heap"
"fmt"
"io"
"os"
"runtime/debug"
)
type Cmp[T any] interface {
Less(T) bool
}
type Heap[T Cmp[T]] struct {
ans []T
}
func (curr *Heap[T]) Len() int {
return len(curr.ans)
}
func (curr *Heap[T]) Swap(x int, y int) {
curr.ans[x], curr.ans[y] = curr.ans[y], curr.ans[x]
}
func (curr *Heap[T]) Less(i, j int) bool {
return curr.ans[i].Less(curr.ans[j])
}
func (curr *Heap[T]) Push(x any) {
curr.ans = append(curr.ans, x.(T))
}
func (curr *Heap[T]) Pop() any {
old := curr.ans
n := len(old)
x := old[n-1]
curr.ans = old[0 : n-1]
return x
}
func NewHeap[T Cmp[T]]() *Heap[T] {
return &Heap[T]{}
}
type Pair[T cmp.Ordered] struct {
x, y T
}
func NewPair[T cmp.Ordered](x T, y T) Pair[T] {
return Pair[T]{x, y}
}
func (curr Pair[T]) Less(other Pair[T]) bool {
if curr.x == other.x {
return curr.y < other.y
}
return curr.x < other.x
}
type M int
func (curr M) Less(other M) bool {
return curr < other
}
func Solve(_r io.Reader, _w io.Writer) {
//in := bufio.NewReader(_r)
out := bufio.NewWriter(_w)
defer out.Flush()
//-----------------------------------------------------
var a = NewHeap[M]()
heap.Push(a, M(454))
heap.Push(a, M(456))
heap.Push(a, M(487))
heap.Push(a, M(787978))
heap.Push(a, M(787798897))
for a.Len() > 0 {
fmt.Println(heap.Pop(a))
}
}
func main() {
debug.SetGCPercent(-1)
Solve(os.Stdin, os.Stdout)
}是不是感觉很easy啊,目前的常见算法平台atcoder已经开始测试新语言版本(go是1.20!!!)
// The max built-in function returns the largest value of a fixed number of
// arguments of [cmp.Ordered] types. There must be at least one argument.
// If T is a floating-point type and any of the arguments are NaNs,
// max will return NaN.
func max[T cmp.Ordered](x T, y ...T) T
// The min built-in function returns the smallest value of a fixed number of
// arguments of [cmp.Ordered] types. There must be at least one argument.
// If T is a floating-point type and any of the arguments are NaNs,
// min will return NaN.
func min[T cmp.Ordered](x T, y ...T) T
// The print built-in function formats its arguments in an
// implementation-specific way and writes the result to standard error.
// Print is useful for bootstrapping and debugging; it is not guaranteed
// to stay in the language.
func print(args ...Type)
// The println built-in function formats its arguments in an
// implementation-specific way and writes the result to standard error.
// Spaces are always added between arguments and a newline is appended.
// Println is useful for bootstrapping and debugging; it is not guaranteed
// to stay in the language.
func println(args ...Type)???金色传说,新版本的Go是很值得去研究和玩的,越来越多的东西加入(说不定哪天手写的旋转treap舍去,官方内置了平衡树),最后的最后,再贴一个官方的优先队列的实现:
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This example demonstrates a priority queue built using the heap interface.
package heap_test
import (
"container/heap"
"fmt"
)
// An Item is something we manage in a priority queue.
type Item struct {
value string // The value of the item; arbitrary.
priority int // The priority of the item in the queue.
// The index is needed by update and is maintained by the heap.Interface methods.
index int // The index of the item in the heap.
}
// A PriorityQueue implements heap.Interface and holds Items.
type PriorityQueue []*Item
func (pq PriorityQueue) Len() int { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool {
// We want Pop to give us the highest, not lowest, priority so we use greater than here.
return pq[i].priority > pq[j].priority
}
func (pq PriorityQueue) Swap(i, j int) {
pq[i], pq[j] = pq[j], pq[i]
pq[i].index = i
pq[j].index = j
}
func (pq *PriorityQueue) Push(x any) {
n := len(*pq)
item := x.(*Item)
item.index = n
*pq = append(*pq, item)
}
func (pq *PriorityQueue) Pop() any {
old := *pq
n := len(old)
item := old[n-1]
old[n-1] = nil // avoid memory leak
item.index = -1 // for safety
*pq = old[0 : n-1]
return item
}
// update modifies the priority and value of an Item in the queue.
func (pq *PriorityQueue) update(item *Item, value string, priority int) {
item.value = value
item.priority = priority
heap.Fix(pq, item.index)
}
// This example creates a PriorityQueue with some items, adds and manipulates an item,
// and then removes the items in priority order.
func Example_priorityQueue() {
// Some items and their priorities.
items := map[string]int{
"banana": 3, "apple": 2, "pear": 4,
}
// Create a priority queue, put the items in it, and
// establish the priority queue (heap) invariants.
pq := make(PriorityQueue, len(items))
i := 0
for value, priority := range items {
pq[i] = &Item{
value: value,
priority: priority,
index: i,
}
i++
}
heap.Init(&pq)
// Insert a new item and then modify its priority.
item := &Item{
value: "orange",
priority: 1,
}
heap.Push(&pq, item)
pq.update(item, item.value, 5)
// Take the items out; they arrive in decreasing priority order.
for pq.Len() > 0 {
item := heap.Pop(&pq).(*Item)
fmt.Printf("%.2d:%s ", item.priority, item.value)
}
// Output:
// 05:orange 04:pear 03:banana 02:apple
}现在来看是不是很easy啊?就在上文提到的example_pq_test.go文件中
制作不易,如有口误,欢迎评论区指正~