基于二叉堆的优先队列和堆排序(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<N && less(j, j+1) {
            j += 1
        }
        if less(j, k) {
            break
        }
        exch(k, j)
        k = j
    }
}

算法使用的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<q.s&&q.less(j, j+1) {
            j += 1
        }
        if !q.less(k, j) {
            break
        }
        q.exch(k, j)
        k = j
    }
}

func (q *MaxPQ) isEmpty() bool {  
    return q.s == 0
}

func (q *MaxPQ) size() int {
    return q.s
}

func (q *MaxPQ) less(i, j int) bool {
    return q.source[i] < q.source[j]
}

func (q *MaxPQ) exch(i, j int) {
    q.source[i], q.source[j] = q.source[j], q.source[i]
}

func (q *MaxPQ) show() {
    var (
        i = 0
    )
    for i<=q.s {
        fmt.Fprintf(os.Stdout, "%v ", q.source[i])
        i++
    }
    fmt.Println()
}

索引优先队列

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

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<imq.N && imq.less(j+1, j) {
            j += 1
        }
        if imq.less(k, j) {
            break
        }
        imq.exch(k, j)
        k = j
    }
}

func (imq *IndexMinPQ) less(i, j int) bool {
    return imq.keys[imq.pq[i]] < imq.keys[imq.pq[j]]
}

func (imq *IndexMinPQ) exch(i, j int) {
    imq.pq[i], imq.pq[j] = imq.pq[j], imq.pq[i]
    imq.qp[imq.pq[i]], imq.qp[imq.pq[j]] = imq.qp[imq.pq[j]], imq.qp[imq.pq[i]]
}

func (imq *IndexMinPQ) show() {
    for i:=1; i<=imq.N; i++ {
        fmt.Printf("%v ", imq.keys[imq.pq[i]])
    }
    fmt.Println()
}

func (imq *IndexMinPQ) min() int {
    return imq.keys[imq.pq[1]]
}

func (imq *IndexMinPQ) contains(k int) bool {
    return imq.qp[k] != -1
}

堆排序

有了上面优先队列的例子, 再看堆排序就很简单了。
堆排序算法分为两阶段:在堆的构造阶段,将原始数组重新组织到一个堆中,然后在下沉阶段,从堆中按递减顺序取出所有元素并得到排序结果。
在堆的构造阶段,可以从左到右遍历数组,用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<length&&less(source, x, x+1) {
            x += 1
        }
        if !less(source, i, x) {
            break
        }
        exch(source, i, x)
        i *= 2
    }
}

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

多叉堆

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容