上一篇博客介绍了堆的基本定义,大家可以看看,然后继续学习本篇。
这篇博客主要讲堆的一个重要应用:堆排序。堆排序是一种原地的、时间复杂度为O(nlogn)的排序算法,我们之前讨论过快速排序,他的时间复杂度也是O(nlogn),那么他和堆排序相比,稳定性不比他好,但是为什么实际开发中用快速排序的地方更多呢?大家带着这个问题来读读本篇博客吧?
关于快速排序的链接我给大家放在这,可以去看看:快速排序
堆排序之建堆
借助于堆这种数据结构实现的排序算法,称为堆排序。堆排序的整个过程可以大致分解成两个大步骤:建堆和排序
先来看看如何建堆
建堆就是将数组中的数据原地组织成一个堆,原地的意思是在不需要额外的存储空间的情况下,在原数组上完成建堆的操作。建堆这里有两种实现的思路,先来看看第一种思路。
第一种思路是上篇博客讲的堆的插入的思路。我们将整个数组的数据划分为前后两个部分,前半部分表示已经建好的堆的数据,后半部分表示非堆中的数据。刚开始,数组中只包含一个数据,那就是下标为1的元素,然后将下标为2到n的元素依次插入到堆中,执行完成之后,数组中的所有数据就被组织成堆这种数据结构了。这种方法采用的从前往后的处理数据,并且对每个插入的数据进行自下而上的堆化。
第二种思路和第一种相反,采用从后往前处理数据,并且对每个数据执行从上自下的堆化方式,相比于第二种思路,第一种思路的执行效率较低。因为叶子节点不需要堆化,对于完全二叉树,下标n/2+1到n的节点都是叶子节点,所有只需要对下标n/2到1的节点进行堆化就可以了。
下面从图理解第二种思路的建堆过程
看看第二种思路的代码实现。
private static void buildHeap(int[] a,int n){
for(int i = n/2;i > 1;--i){
heapify(a,n,i);
}
}
private static void heapify(int[] a,int n,int i){
while(true){
int maxPos = i;
if(i/2 < n && a[i/2] < a[2*i]) maxPos = 2*i;
if(i/2 < n && a[i/2] < a[2*i+1]) maxPos = 2*i+1;
if(maxPos == i) break;
swap(a,i,maxPos);
i = maxPos;
}
}
现在再来看看第二种思路,建堆操作的时间复杂度
之前说过,每个节点堆化的时间复杂度为O(logn),那么n/2个节点堆化的总的时间复杂度是不是就是O(nlogn)呢?答案虽然没错,但是不够精确,实际上,第二种思路的时间复杂度为O(n)。具体的推导过程在这里我就不详细介绍了,大家只需要记住第二种实现思路的时间复杂度是O(n)就可以了。
现在再来看看第一种实现思路的时间复杂度,每个节点都需要进行堆化,不过,与第二种实现思路不同的是,每个节点执行的是自下而上的堆化操作,总的堆化时间等于每个节点的个数乘以节点的深度(深度不同于高度,深度指的是从这个节点到根节点的路径长度)。所以,第一种实现思路的时间复杂度为O(ologn)。
堆排序之排序
在建堆结束之后,数组中的数据已经是按照大顶堆的特性来组织的了。数组中的第一个元素是堆顶元素,也是最大的元素。我们把他与最后一个元素交换,那么最大的元素就到了数组下标为n的位置,交换之后下标为1的元素还要进行自上而下的堆化过程。当堆化完成之后,堆中现在就剩余n-1个元素,然后继续进行交换,直到队中只有一个元素的时候,排序工作就结束了。
先来看看图解。
下面看看代码实现
private static void sort(int[] a,int n){
buildHeap(a,n);
int k = n;
while(k > 1){
swap(a,1,k);
--k;
heapify(a,k,1);
}
}
堆排序的性能分析
现在来看看堆排序的时间复杂度,空间复杂度和稳定性
堆排序包括建堆和排序两个操作,这两个操作都是在原数组上进行的,因此,堆排序是原地排序算法。堆排序也不需要递归操作,所有的操作中只需要常量级的存储空间,所以,空间复杂度为O(1)。
基于第一种实现思路的时间复杂度为O(nlogn),基于第二种实现思路的时间复杂度是O(n)。排序过程需要执行n次自上而下的堆化,时间复杂度的计算类似基于第一种实现思路的建堆过程的时间复杂度计算,因此堆排序总体时间复杂度为O(nlogn)。
堆排序不是稳定性排序,因为在排序过程中,存在最后一个节点和第一个节点交换的过程,这有可能改变值相同数据原有的先后顺序。
快速排序为什么比堆排序的性能好
第一:堆排序的数据访问方式没有快速排序高效
对于快速排序,数据是顺序访问的,而对于堆排序,数据是随机访问的。因为我们直到,堆的核心操作就是堆化,从下面的图可以看出来,对堆顶元素进行堆化,会依次访问数组下标是1,2,4,8元素,他是随机访问的。因此,堆排序无法利用CPU缓存预读数据,所以性能方面会有所下降。
第二:对于同样的数据,堆排序数据交换的次数要多于快速排序
这里会出现两个概念,有序度和无序度。我在我的博客中,排序有讲到,大家可以去看看。对于 基于比较的排序算法,整个排序过程是由比较和交换两个基本操作完成的。快速排序中数据交换的次数等于逆序度。而堆排序的第一步是建堆,建堆的过程会打乱数据原有的排序顺序,数据反而变得无序了。,建堆导致有序度降低。
最后喜欢的话大家给留个赞,嘿!