堆排序的比较次数_堆排序过程、时间复杂度分析及改进

945dc465b7b310d488837b969decdfcb.png

前言

前面介绍过四种排序方法,其中快速排序和归并排序的平均时间复杂度都是

equation?tex=O%28nlog%28n%29%29 量级,今天要介绍堆排序,也是一种
equation?tex=O%28nlog%28n%29%29 平均复杂度的排序算法,这种排序算法是基于完全二叉树实现的,如果还不熟悉完全二叉树的同学,请自行查阅相关数据结构的知识,这里关注于算法的分析和改进过程。下面我们开始。

一、堆排序过程

堆排序有两种形式,一种是大顶堆,一种是小顶堆。顾名思义,大顶堆的父结点总大于子结点,小顶堆父结点总小于子结点。

51877b9285659d099d64bc84741f471e.png
大顶堆与小顶堆

建立这个堆的过程有两种方法,分别是筛选法和插入法。

1. 建堆过程分析

① 筛选法

  • 过程

假设当前有一个数字序列为

2556497811654136

先对这个序列建立完全二叉树。

c423f1b8b80b9cfcbd0d212d24c66439.png
完全二叉树

随后从这n个元素的n/2位置开始,进行堆调整,每次调整把两个子结点中最大的而且大于父结点的节点数字与父结点交换,直到调整到根节点为止。

最后我们会得到最终的调整结果。

a004f8aa9d0e2a0ec05274dad60bc6c8.png
筛选法建堆结果

下面给出一个动画演示筛选法建堆的过程以及后续堆排序的过程。

9f482bf3363b9ee2e75ddafd852a8ae8.gif
筛选法建堆过程
public class Heapsort {

    private void headAdjust(int[] array, int start, int stop){
        int j = 0;
        array[0] = array[start];
        for (j = 2*start;j<=stop;j=j*2){
            if(j<stop && array[j]<array[j+1])j++;
            if(array[0]>=array[j])break;
            array[start] = array[j];
            start = j;
        }
        array[start] = array[0];
    }
    private void headsort(int[] array){
        int i,j,k;
        //筛选法建堆
        for (i = array.length/2;i>0;i--){
            headAdjust(array,i,array.length-1);
        }
        //堆排序过程
        for(j = array.length-1;j>1;j--){
            array[0] = array[j];
            array[j] = array[1];
            array[1] = array[0];
            headAdjust(array,1,j-1);
        }
        headAdjust(array,i,array.length-1);
    }


    public static void main(String[] args) {
        int[] array = new int[]{0,25,56,49,78,11,65,41,36};//0位置作为哨兵项
        Heapsort headsort = new Heapsort();
        headsort.headsort(array);
        for(int item: array){
            System.out.print(item+" ");
        }
    }
}
  • 时间复杂度分析

经过上面的分析我们可以观察得出如下几点规律:

堆的高度为k,最多有
equation?tex=2%5Ek-1 个结点。

调整k-1层的数据结点,最多下调一层,以此类推。
每下调一层,需要进行两次元素比较,一次是孩子之间比较,选出最大值,然后用这个最大值和父结点比较,决定是否交换父结点。

输入n个元素建堆,需要最多的比较次数为:

equation?tex=%5Cbegin%7Balign%7D+S%28k%29+%26%3D+2%2A2%5E%7Bk-2%7D%2A1%2B2%2A2%5E%7Bk-3%7D%2A2%2B...%2B2%2A2%5E0%2A%28k-1%29%5C%5C+%26%3D2%282%5E%7Bk-2%7D%2A1%2B2%5E%7Bk-3%7D%2A2%2B...%2B2%5E0%2A%28k-1%29%29+%5Cend%7Balign%7D

equation?tex=S_k+%3D+2%5E%7Bk-2%7D%2A1%2B2%5E%7Bk-3%7D%2A2%2B...%2B2%5E0%2A%28k-1%29 则我们有

equation?tex=2S_k-S_k+%3D+1-k%2B2%2B2%5E2%2B...%2B2%5E%7Bk-1%7D%3D2%5Ek-1-k

接着由我们第一条规律得到

equation?tex=S%28k%29+%3D+2S_k+%3D+2%282%5Ek-1-k%29%5Capprox+2%28n-logn%29%5Cle+2n

因此,筛选法建堆最坏情况的时间复杂度为

equation?tex=O%28n%29 量级。

② 插入法

  • 过程

插入法建堆过程和筛选法不太一样,下面先了解一下插入法建堆过程。

插入法是从无到有开始建堆,不同于筛选法基于完全二叉树调整建堆。插入法在插入一个新元素是,只和父结点比较,无需和兄弟结点比较,如果大于父结点则上调一层,比较一次。

a7fd2dcbebf993a3600fba4b4f9cfb54.gif
插入法建堆过程

这里不再给出插入法建堆过程的代码。我们直接进行时间复杂度的分析。

时间复杂度分析

类似于筛选法,我们分析最坏情况时间复杂度。有下面几个事实

插入一个新元素,只与父结点比较一次,比较一次,最多上调一层,最多上调当前层减一。
二叉树第i层有
equation?tex=2%5E%7Bi-1%7D 个结点,每个结点最多上调i-1层。

则输入n个元素建堆需要的最多比较次数为

equation?tex=%5Csum_%7Bi%3D2%7D%5E%7Blogn%7D2%5E%7Bi-1%7D%5Ctimes%28i-1%29

equation?tex=S%28n%29+%3D+1%2A2%2B2%2A2%5E2%2B3%2A2%5E3%2B...%2B%28logn-1%29%2A2%5E%7Blogn-1%7D%5C%5C+2S%28n%29+%3D+1%2A2%5E2%2B2%2A2%5E3%2B3%2A2%5E4%2B...%2B%28log-1%29%2A2%5E%7Blogn%7D%5C%5C+2S%28n%29-S%28n%29%3D2%2B2%5E2%2B2%5E3%2B...%2B2%5E%7Blogn-1%7D-%28logn-1%292%5E%7Blogn%7D+%3Dnlogn-2n%2B2+

可以看出插入法建堆的过程时间复杂度为

equation?tex=O%28nlogn%29 量级。

我们通常不适用插入法建堆,而通常使用筛选法建堆,因为筛选法时间复杂度O(n),而插入法时间复杂度为O(nlogn)。

2. 堆调整过程分析

  • 堆调整过程

堆调整的过程如上面的堆排序整体过程所示的那样,这里不在赘述,下面默认大家都已经完全理解了堆排序的过程。我们对堆调整过程进行一下时间复杂度分析。

  • 堆调整时间复杂度分析

堆排序调整过程,每个元素都要遍历一次,因此每个元素都有被上调的概率,我们假设每个元素都下调其原所在层数,则有如下公式

equation?tex=2%5Csum_%7Bk%3D1%7D%5E%7Bn-1%7D%5Clfloor+logk%5Crfloor%5Cle2%5Cint_1%5Enlog_2e%5Ccdot+ln%28x%29dx%3D2nlogn-2%2A1.443n

我们可知堆排序筛选法建堆和堆调整过程结合到一起,时间复杂度是O(n)+O(nlogn),进一步堆排序时间复杂度为O(nlogn)量级。

二、堆排序的改进

显然堆排序的调整过程是存在问题的,还是存在可以继续优化的空间。

我们观察到,堆调整过程每个元素上调都进行了两次比较操作,我们考虑能否精简这两次比较,使其小于2,最理想能达到1。

下面介绍两种堆排序的加速方式:

第一种堆排序加速方式:

e6983cbd72c1fd66bed8fd8adeedbcce.png
堆排序

上面的大顶堆,我们首先将38与113交换,然后并不将38做调整,反而我们将根节点的孩子节点中较大的那个调整到父结点,最后我们会得到如下的大顶堆。

21003908aa8972c878d2453ce5be9650.png
下沉过程

这整个过程,每次元素的调整只进行了一次元素比较。

接下来,我们进行一遍向上调整,38不断和父结点比较,如果发生交换继续与交换后的父结点比较。

通常,向上比较的次数不会超过下沉操作的比较次数,正常情况下向上调整的次数不多。整个算法整体堆调整过程的时间复杂度,可能只有一点几倍的nlogn,优于2nlogn的时间复杂度。

第二种堆排序加速过程:

第一种堆排序过程仍然存在可以优化的空间。我们关注于第一步元素下沉的过程,如果我们能够在合适的位置停止下沉,则不需要进行向上调整,会进一步减少比较次数。

一般我们会在下沉一半时,判断是否需要继续下沉。

总结

以上就是堆排序相关的过程,时间复杂度分析和改进。堆排序从时间复杂度上来说,不失为一种优秀的排序算法。最坏时间复杂度是nlogn量级的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值