非递归的堆排序

0x01 背景

多年之前写了一篇博客,实现非递归方式的堆排序,多年之后才发现写的有问题。连忙撤了下来,重新复习了《算法导论》,用Go实现了正确的非递归排序。算是填上了当年的坑。基本上是复原了书上的实现,加上自己的理解。

0x02 堆与堆排序

堆是一种完全二叉树。它具有如下特点:

每个节点都比其他子节点的值大或者小。

都大的叫大根堆,都小的叫小根堆。反过来表达,除根节点外,其他节点都比父节点小或者大。堆可以和一个数组对应起来。下标为i的元素的两个子节点分别为2i+1和2i+2。
在这里插入图片描述

堆排序就是利用堆的构建过程进行的排序。比如构建一个大根堆,然后将根与数组的最后一个元素交换,将堆的大小减1之后,再将剩下的元素恢复一个大根堆,这样重复到最后,堆的大小变为1,就完成了排序过程。

0x03 为什么之前的理解有问题?

之前的理解受到了二叉搜索树的查找过程的误导,认为只要递归地比较交换每个节点和它的两个子节点,就能完成建堆的过程。
其实这样是有问题的,这样递归构建时,是深度优先的。先构建h层,然后是h-1层。但h-1交换元素时,可能就破坏了之前已经执行过的h层堆的性质。同样,非递归实现时,也没有考虑到这个问题,所以也是有问题的。

如下图,第一次将3和6交换。递归到第1层时,将8和3交换,最终第2层的堆性质被破坏。
在这里插入图片描述

0x04 正确的非递归实现

实现很简单,将书中MAX-HEAPIFY修改为循环就行了。考虑到Go中也可以使用goto,最终为:


package sort

import (
    stdsort "sort"
)


func leftChild(i int) int {
    return i * 2 + 1
}

func rightChild(i int) int {
    return i * 2 + 2
}

func maxHeapify(data stdsort.Interface, i, end int) {

retry_:

        l := leftChild(i)
        r := rightChild(i)

        largest := i

        if l <= end && data.Less(i, l) {
                largest = l
        }
        if r <= end && data.Less(largest, r) {
                largest = r
        }

        if largest != i {
            data.Swap(i, largest)
            i = largest
            goto retry_
        }
}

// buildMaxHeap builds a heap in a non-recursive way.
func buildMaxHeap(data stdsort.Interface, start, end int) {
        e := (end+1)/2 - 1
        for e >= start {
            maxHeapify(data, e, end)
            e--
        }
}

// HeapSort sorts an array in inc order.
func HeapSort(data stdsort.Interface) {
        end := data.Len() - 1
        buildMaxHeap(data, 0, end)
        for i := 0; i < end; i++ {
            data.Swap(0, end-i)
            maxHeapify(data, 0, end-i-1)
        }
}

0x05 使用场景

堆排序是原址不稳定排序。原因是在交换父子节点值时,是跨层交换的,可能将一个节点与同值节点的先后顺序打乱。
但堆也有其独特的应用场景:

  1. 优先队列。
    与堆排序过程一样。首先,构建出一个堆。
    • 需要出队时,将根出队,将队尾元素放到根位置,重新运行一次HEAPIFY,恢复堆。时间复杂度为O(lg(n))。
    • 入队时,将元素放到队列尾部,沿此元素到根的路径上进行比较交换,直到找到合适的位置。时间复杂度同样为O(lg(n))。
  2. 最大k值。
    海量数据的最大k值问题。堆非常适合这种场景。真的要去比较所有数据,构建有序数列的话,运算量会非常的大。而通过构建大小为K的小根堆,可以快速完成。数量量为n的话。快速排序的复杂度为O(nlg(n)。而使用堆的话,就是:O(klg(k)) + O(nlg(k)),在k远小于n的情况下最终为:O(nlg(k))。
    实现过程也简单,先使用前k个数据构建出小根堆,遍历剩下的元素,如果大于根,就把与根交换,执行一个HEAPIFY恢复堆。这样直到所有数据遍历完成,剩下的堆就是最大的K个元素。

0x06 总结

对于算法要有严格的理解,经典教材里的原理一定要理解到位。否则可能在后面的编码生涯中挖坑埋雷。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值