基于二叉堆的优先队列和堆排序(golang实现)


二叉堆

堆有序定义:当一颗二叉树的每个节点都大于等于它的两个子节点时, 被称为堆有序。
二叉堆定义: 二叉堆是一组能够用堆有序的完全二叉树排序的元素,并在数组中按照层级储存(不使用数组中第一个位置)。
在一个二叉堆中,位置k的节点的父节点位置为|k/2|(k/2向下取整),两个子节点的位置分别为:2k、2k+1。
下文中二叉堆简称为堆。
堆的有序化定义:堆的操作会首先进行一些改动,打破堆的状态,然后再遍历堆并按照要求将堆的状态恢复。
在有序化的过程中, 会遇到两种情况:
1、 当某个节点的优先级上升(或者堆底加入新元素),需要由下至上恢复堆的顺序。 算法实现如下:

 func swim(k int) {
    for k>1&&less(k/2, k) {
        exch(k/2, k)
        k /= 2
    }
}

2、 当某个节点的优先级下降(例如, 将根节点替换为一个较小的元素)时, 需要由上至下恢复堆的顺序。
算法实现如下:

func sink(k int) {
    for 2*k <= N {  //N为二叉堆的元素数量
        j := 2*k
        if 2*k

算法使用的exch、less方法:

func less(source []int, i, j int) {
    return source[i] < source[j]
}
func exch(source []int, i, j int) {
    source[i], source[j] = source[j], source[i]
    return
}

优先队列

优先队列使用场景很多, 例如:处理很多程序中优先级最高的程序;收集一些数据,处理当前最大/最小的元素,再收集一些数据,再处理当前最大/最小的数据等等。
这些使用场景可以抽象为从N个输入元素中找到最大/最小的M个元素。可以看一下下面表格中一些方法实现这种需求需要的资源,特别是在N非常大的时候,如何利用有限的资源高效的实现是比较大的挑战。

示例 时间 空间
排序算法 NlogN N
初级实现(数组/链表)的优先队列 NM M
堆实现的优先队列 NlogM M

优先队列也是很多重要算法的基础, 例如一些重要的图搜索算法、数据压缩算法等。
优先队列两个最重要的操作是:删除最大(小)元素插入元素。使用上跟队列、栈使用比较相似,在实现上, 和栈、队列最大的不同是: 对于性能上的要求。队列和栈的实现能够在常数时间内完成所有的操作,而对于优先队列,初级实现(有序/无序的数组/链表)在删除元素和插入元素之一操作的最坏情况下需要线性时间完成,而基于二叉堆的实现能够保证这两种操作更快(对数级别)。

基于二叉堆实现的优先队列:
1、 插入操作:
将新元素插入到数组的尾部,增加堆的大小并让这个元素swim(对应上面二叉堆的优先级上升操作)到相应的位置。
2、 删除操作:
从数组顶端删除最大的元素,并将数组中最后一个元素放置到顶端,减小堆的大小, 并让这个元素sink(对应上面二叉堆的优先级下降操作)到响应的位置
具体实现代码:

type MaxPQ struct {
    source []int
    s int
}

func NewMaxPQ(k int) *MaxPQ {
    return &MaxPQ{
        source: make([]int, k+1),
        s: 0,
    }
}

func (q *MaxPQ) insert(key int) { //插入操作
    q.s++
    q.source[q.s] = key
    q.swim(q.s)
}

func (q *MaxPQ) delMax() (x int) { //删除操作
    x = q.source[1]
    q.exch(1, q.s)
    q.s--
    q.sink(1)
    return 
}

func (q *MaxPQ) swim(k int) {
    for k>1&&q.less(k/2, k) {
        q.exch(k/2, k)
        k /= 2
    }
}

func (q *MaxPQ) sink(k int) {
    for 2*k <= q.s {
        j := 2*k
        if j

索引优先队列

索引优先队列在多向归并的使用场景中非常有效, 例如:从多个有序的输入流归并成一个有序序列,如果有足够的空间,你可以简单的把它们读入一个数组并排序,但如果使用了优先队列,无论输入有多长你都可以把他们全部读入并排序。
索引优先队列的实现如下:

type IndexMinPQ struct {
    keys []int //元素存放的数组
    pq []int //二叉堆实现的索引
    qp []int //索引二叉堆的逆序, 可以很方便的做contains判断
    N int //PQ中元素的数量
}
func NewIndexMinPQ(max int) (imq *IndexMinPQ) {
    imq = &IndexMinPQ{
        keys: make([]int, max+1),
        pq: make([]int, max+1),
        qp: make([]int, max+1),
    }
    for i:=0; i<=max; i++ {
        imq.qp[i] = -1  //初始化为-1
    }
    return
}

func (imq *IndexMinPQ) insert(k int, key int) { //插入操作
    imq.N++
    imq.pq[imq.N] = k
    imq.keys[k] = key
    imq.qp[k] = imq.N
    imq.swim(imq.N)
}

func (imq *IndexMinPQ) delMin() (indexOfMin int) {  //删除操作
    indexOfMin = imq.keys[imq.pq[1]]
    imq.exch(1, imq.N)
    imq.N--
    imq.sink(1)
    imq.keys[imq.pq[imq.N+1]] = -1
    imq.qp[imq.pq[imq.N+1]] = -1
    return
}

func (imq *IndexMinPQ) swim(k int) { 
    for k>1 && (!imq.less(k/2, k)) {
        imq.exch(k/2, k)
        k /= 2
    }
}

func (imq *IndexMinPQ) sink(k int) {  
    for 2*k <= imq.N {
        j := 2*k
        if 2*k

堆排序

有了上面优先队列的例子, 再看堆排序就很简单了。
堆排序算法分为两阶段:在堆的构造阶段,将原始数组重新组织到一个堆中,然后在下沉阶段,从堆中按递减顺序取出所有元素并得到排序结果。
在堆的构造阶段,可以从左到右遍历数组,用swim()保证扫描到的位置左侧都是一颗堆有序的完全树,但更高效的方法是:从右至左用sink()函数构造子堆,开始时我们只需要扫描一半的元素,因为我们可以跳过大小为1的子堆。

func heapSort(source []int) {
    var (
        l = len(source)-1
        j = l
    )
    for i:=l/2; i>=1; i-- {  //构造堆阶段,只需要扫描一半的元素
        sink(source, i, l)
    }
    for j>1 {   //下沉阶段
        exch(source, 1, j)
        j--
        sink(source, 1, j)
    }
}

func sink(source []int, i, length int) {
    for 2*i<=length {
        x := 2*i
        if x

less函数和exch函数参照上文。

多叉堆

参考资料: 《算法》第四版

你可能感兴趣的:(基于二叉堆的优先队列和堆排序(golang实现))