堆排序梳理与复杂度分析

之前看了下堆排序,此处的堆感觉和我理解的堆不是一个东西。此文力图探讨以下问题:
1.堆排序中“堆”的含义
2.堆排序算法的实现
3.算法的复杂度分析
4.堆排序蕴含的原理
此处先阐述堆的定义:

堆是一种特殊的树,只要满足下面两个条件,它就是一个堆:
(1)堆是一颗完全二叉树;
(2)堆中某个节点的值总是不大于(或不小于)其父节点的值。
其中,我们把根节点最大的堆叫做大顶堆,根节点最小的堆叫做小顶堆。

在我原先的认知中,堆在计算机方面的意思就是堆栈的堆,指的是由malloc分配的内存所在的那一段内存空间的名字。而此处的堆,明显是一种数据结构,他的全名是二叉堆(Binary Heap),他是二叉树(Binary Tree )的一种特例,即完全二叉树( complete binary tree)。

这里需要插一些关于二叉树的名词解释:

满二叉树Full Binary Tree (国际定义):除了叶子结点之外的每一个结点都有两个孩子结点,任何哈夫曼编码都为满二叉树
满二叉树Full Binary Tree(国内定义):同完美二叉树
完全二叉树Complete Binary Tree:除了最后一层之外的其他每一层都被完全填充,并且所有结点都保持向左对齐
完美二叉树Perfect Binary Tree:除了叶子结点之外的每一个结点都有两个孩子,每一层(当然包含最后一层)都被完全填充

由于完全二叉树的性质,二叉堆可以很方便有效的使用数组来实现。
我还新学了个单词heapify,意思是堆化。我本来还以为这个是heap和ify的结合,ify是某单词的缩写呢。。。
堆排序也是一种选择排序,具体来说就是通过堆化,每趟都可以获得最大/小值,这样就和普通的直接选择排序一样了。
堆排序的实现分为下面几个部分:
(1)创建最大堆(Build Max Heap):将数组转化为堆。建堆有两种方法,筛选法(自上而下),插入法(自下而上)。
(2)最大堆调整(Max Heapify):重新排列一个堆来保持堆属性。需要注意的是其子树必须是堆才能开始。
(3)堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算。

堆化简单来说就是将父节点与值比较大的那个子节点的值比较(假如此时在构建大顶堆),如果子节点值较大,那么为了保持堆的性质(即第二点),需要交换父节点与那个较大的子节点,交换完毕之后,父节点继续与他在新位置的下层子节点迭代上述步骤,直到父节点比子节点大或者最最开始指定的父节点已经成为子节点为止才会停止。
所以说堆化的前提条件是其子树必须是堆,否则执行的比较就不能正确使完全二叉树调整为堆了。

堆的另外两个常见操作为弹出最值与插入节点,类比于删除与添加操作。
弹出最值操作即为取出根节点。根节点被弹出后,为了维持完全二叉树的性质,将最后一个节点移到根节点上作为新的根节点,然后执行堆化操作。
插入操作即为将一个新的节点插入到堆中。新节点插入的位置是堆的最后,为了保持堆的性质,新节点需要和父节点比较,若比父节点小(假如是最大堆),需要与父节点交换继续比较,直到他的值比父节点小或者他成为子节点为止。

堆排序就是在最大堆构建完成之后,执行弹出操作,把弹出的根节点放入尾部,直到排序完成。

关于堆排序的算法复杂度,可以分为两部分来看,即建堆的复杂度和排序的复杂度。
建堆有两种方法,而堆排序实际上就是不停堆化直到排序完成。
筛选法建堆其实也是利用了堆化,需要从最后的子节点依次往前,自上而下,进行堆化操作,直到处理到根,此时建堆完成。
而插入法则是将数组成员看做一直插入操作,当最后一个元素被插入后,建堆完成。
筛选法实际是执行了总父节点数的堆化,假设二叉堆有共有k层,当前层数为i,则总父节点数为2(k-1)-1,每层有2(i-1)个节点.堆化操作中每次需要进行两次比较(子节点间,较大子节点与父节点间),共执行i-1次操作。则堆化的最差复杂度为2*(i-1),最好为2。则筛选法建堆最差复杂度为2*(k-1)+212(k-2)+222(k-3)+…2k-32*2+2k-22=2*(k-1)+22(k-2)+23(k-3)+…2k-2*2+2k-1
令上式为s(k),s(k)=2s(k)-s(k)=-2(k-1)+22+23+2k-1+2k,根据等比数列等,最后得s(k)=2k+1-2k-2.
节点数n满足n<=2k-1,所以n<=2k,所以s(k)=2k+1-2k+2>=2k+1>=2n
所以最坏时间复杂度至少为O(n)。
最好复杂度即数组本身有序,复杂度为父节点数乘2即为2k-2,即2n-2,则最好复杂度为O(n)。
至于堆排序,其实就是一直执行堆化,堆化复杂度即为O(logn),则堆排序复杂度即log1+log2+log3+logn=logn!。根据斯特林公式公式可推得,logn!与nlogn为等价无穷大。所以堆排序复杂度为log(n)+nlog(n),即为nlog(n)。
插入法建堆,每次插入操作,最坏复杂度为O(logn),最好复杂度为O(1)。则最坏时间复杂度为每层节点数2k-1与比较层数k-1乘积的累加:2*1+22*2+…2k-1(k-1),令此式为s(k)则,s(k)=2s(k)-s(k)=-(2+22+23+…2k-1)+2k(k-1),由等比数列,s(k)=2-2k+2k(k-1)=+2k(k-2)+2,则其最坏复杂度为O(nlogn).而最好复杂度为O(n)。
堆排序最重要的应当是堆化操作,不管是建堆和调整堆都会用到它,而堆化操作由于堆的性质,每次最差也只需要O(logn)的复杂度既能完成。而要想使这种以比较为基础的算法向其最优理论复杂度nlog(n)靠拢,就需要尽可能均等策略,快排和堆排复杂度都为O(nlogn),然而一般情况下快排都比堆排快,这是因为传统堆排序的时的堆化,策略是不均等的。堆化每次弹出根节点后,将最后一个根节点移到首节点,其实此时根节点的值是很大概率比他们的子节点小的,这就导致策略不均等,每次需要向下比较多层的概率很大。所以针对堆排序的优化也是从这方面着手:

MacKay也提供了一个修改版的堆排:每次不是将堆底的元素拿到上面去,而是直接比较堆顶(最大)元素的两个儿子,即选出次大的元素。由于这两个儿子之间的大小关系是很不确定的,两者都很大,说不好哪个更大哪个更小,所以这次比较的两个结果就是概率均等的了。

当然还有另一种说法是,读取cache导致堆排不如快排,供参考。
堆排序为什么比选择,插入,冒泡那些O(n^2)的算法快呢?因为排序就是消除逆序对的过程,复杂度O(n^2)的算法,消除了n个逆序对需要执行n次操作,而堆排序消除n个逆序对只需要logn次操作。

参考:
1.堆排序算法描述及时间复杂度分析
2.希尔排序为什么会那么牛那么快,能够证明吗?
3.数学之美番外篇:快排为什么那样快

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值