![945dc465b7b310d488837b969decdfcb.png](https://i-blog.csdnimg.cn/blog_migrate/1078693cd8067e0d73e01f5d9091147d.jpeg)
前言
前面介绍过四种排序方法,其中快速排序和归并排序的平均时间复杂度都是
![equation?tex=O%28nlog%28n%29%29](https://i-blog.csdnimg.cn/blog_migrate/50bb8d87262646c79ddc9b52cc64bacf.png)
![equation?tex=O%28nlog%28n%29%29](https://i-blog.csdnimg.cn/blog_migrate/50bb8d87262646c79ddc9b52cc64bacf.png)
一、堆排序过程
堆排序有两种形式,一种是大顶堆,一种是小顶堆。顾名思义,大顶堆的父结点总大于子结点,小顶堆父结点总小于子结点。
![51877b9285659d099d64bc84741f471e.png](https://i-blog.csdnimg.cn/blog_migrate/04311967b60ce31a04715269504a46ab.jpeg)
建立这个堆的过程有两种方法,分别是筛选法和插入法。
1. 建堆过程分析
① 筛选法
- 过程
假设当前有一个数字序列为
25 | 56 | 49 | 78 | 11 | 65 | 41 | 36 |
---|
先对这个序列建立完全二叉树。
![c423f1b8b80b9cfcbd0d212d24c66439.png](https://i-blog.csdnimg.cn/blog_migrate/42e183b777436a9c8cc7ce2833436c54.jpeg)
随后从这n个元素的n/2位置开始,进行堆调整,每次调整把两个子结点中最大的而且大于父结点的节点数字与父结点交换,直到调整到根节点为止。
最后我们会得到最终的调整结果。
![a004f8aa9d0e2a0ec05274dad60bc6c8.png](https://i-blog.csdnimg.cn/blog_migrate/a16951e6c89ff254501689406dd7a773.jpeg)
下面给出一个动画演示筛选法建堆的过程以及后续堆排序的过程。
![9f482bf3363b9ee2e75ddafd852a8ae8.gif](https://i-blog.csdnimg.cn/blog_migrate/615f43e6226576ffd35d99848c372c73.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,最多有个结点。
调整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](https://i-blog.csdnimg.cn/blog_migrate/f83a04c7152f613fe29cea1d8bb42fee.png)
令
![equation?tex=S_k+%3D+2%5E%7Bk-2%7D%2A1%2B2%5E%7Bk-3%7D%2A2%2B...%2B2%5E0%2A%28k-1%29](https://i-blog.csdnimg.cn/blog_migrate/ce630be1c509f66b82d6bc76d1f17e80.png)
![equation?tex=2S_k-S_k+%3D+1-k%2B2%2B2%5E2%2B...%2B2%5E%7Bk-1%7D%3D2%5Ek-1-k](https://i-blog.csdnimg.cn/blog_migrate/813353bd51fb9f32a48edc8d06d7d751.png)
接着由我们第一条规律得到
![equation?tex=S%28k%29+%3D+2S_k+%3D+2%282%5Ek-1-k%29%5Capprox+2%28n-logn%29%5Cle+2n](https://i-blog.csdnimg.cn/blog_migrate/738dec75312e43a09d3ed0d9a06c5db2.png)
因此,筛选法建堆最坏情况的时间复杂度为
![equation?tex=O%28n%29](https://i-blog.csdnimg.cn/blog_migrate/b1e1fe9d316f093d33735a5259d8e5e5.png)
② 插入法
- 过程
插入法建堆过程和筛选法不太一样,下面先了解一下插入法建堆过程。
插入法是从无到有开始建堆,不同于筛选法基于完全二叉树调整建堆。插入法在插入一个新元素是,只和父结点比较,无需和兄弟结点比较,如果大于父结点则上调一层,比较一次。
![a7fd2dcbebf993a3600fba4b4f9cfb54.gif](https://i-blog.csdnimg.cn/blog_migrate/f371bd8fd4be189f7272aa61c1694ba1.gif)
这里不再给出插入法建堆过程的代码。我们直接进行时间复杂度的分析。
时间复杂度分析
类似于筛选法,我们分析最坏情况时间复杂度。有下面几个事实
插入一个新元素,只与父结点比较一次,比较一次,最多上调一层,最多上调当前层减一。
二叉树第i层有个结点,每个结点最多上调i-1层。
则输入n个元素建堆需要的最多比较次数为
![equation?tex=%5Csum_%7Bi%3D2%7D%5E%7Blogn%7D2%5E%7Bi-1%7D%5Ctimes%28i-1%29](https://i-blog.csdnimg.cn/blog_migrate/ac61de3a50c237df8449261eabeb652a.png)
![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+](https://i-blog.csdnimg.cn/blog_migrate/4388812cdda238cc6d1fde0bd2e45c94.png)
可以看出插入法建堆的过程时间复杂度为
![equation?tex=O%28nlogn%29](https://i-blog.csdnimg.cn/blog_migrate/a9536f37d37c78d9a48a69d6fd939c4d.png)
我们通常不适用插入法建堆,而通常使用筛选法建堆,因为筛选法时间复杂度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](https://i-blog.csdnimg.cn/blog_migrate/8fa91a47334f6db10e877bf620fc2099.png)
我们可知堆排序筛选法建堆和堆调整过程结合到一起,时间复杂度是O(n)+O(nlogn),进一步堆排序时间复杂度为O(nlogn)量级。
二、堆排序的改进
显然堆排序的调整过程是存在问题的,还是存在可以继续优化的空间。
我们观察到,堆调整过程每个元素上调都进行了两次比较操作,我们考虑能否精简这两次比较,使其小于2,最理想能达到1。
下面介绍两种堆排序的加速方式:
第一种堆排序加速方式:
![e6983cbd72c1fd66bed8fd8adeedbcce.png](https://i-blog.csdnimg.cn/blog_migrate/f2b39e11ab93249d1745dba5c2cbd16c.jpeg)
上面的大顶堆,我们首先将38与113交换,然后并不将38做调整,反而我们将根节点的孩子节点中较大的那个调整到父结点,最后我们会得到如下的大顶堆。
![21003908aa8972c878d2453ce5be9650.png](https://i-blog.csdnimg.cn/blog_migrate/cf864b87d28a2fb6b822275e8449d0af.jpeg)
这整个过程,每次元素的调整只进行了一次元素比较。
接下来,我们进行一遍向上调整,38不断和父结点比较,如果发生交换继续与交换后的父结点比较。
通常,向上比较的次数不会超过下沉操作的比较次数,正常情况下向上调整的次数不多。整个算法整体堆调整过程的时间复杂度,可能只有一点几倍的nlogn,优于2nlogn的时间复杂度。
第二种堆排序加速过程:
第一种堆排序过程仍然存在可以优化的空间。我们关注于第一步元素下沉的过程,如果我们能够在合适的位置停止下沉,则不需要进行向上调整,会进一步减少比较次数。
一般我们会在下沉一半时,判断是否需要继续下沉。
总结
以上就是堆排序相关的过程,时间复杂度分析和改进。堆排序从时间复杂度上来说,不失为一种优秀的排序算法。最坏时间复杂度是nlogn量级的。