“这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战”
关注我,以下内容持续更新
前言
在介绍堆排序之前,先简单回顾下完全二叉树和堆
-
完全二叉树:如果二叉树中除去最后一层节点为满二叉树,且最后一层的结点依次从左到右分布,则此二叉树被称为完全二叉树。
-
堆是一棵顺序存储的完全二叉树,堆的存储一般用数组来实现
-
大顶堆: 每个结点的值都大于或等于其左右孩子结点的值
-
小顶堆: 每个结点的值都小于或等于其左右孩子结点的值
-
关于完全二叉树和堆,具体的可以看数据结构与算法(七):树, 明天会更新
堆排序思路
堆排序是一种选择排序,可以用一维数组顺序存储堆. 那么如何利用堆进行排序呢?拿升序排序来讲,先把待排序序列按照从上至下从左至右的顺序构建成一个大顶堆,那么大顶堆的根节点就是最大的元素,把根节点取出来,再把剩下的元素再构造成一个新堆(如何保证取走最大值更高效的构建新堆呢,就是把根节点arr[0]和最后一个叶子节点arr[n-1]进行调换,然后取走最后一个叶子节点arr[n-1],此时堆不满足大顶堆的条件,只能说它是一个完全二叉树,所以要递归调整完全二叉树使它成为一个大顶堆),同样满足根节点是最大的元素,此时再取出根节点,继续构建新堆,循环执行.
例如把数组{100,33,3,7,11,6,8,5}进行升序排序的过程如下:
第一步:构建完全二叉树
首先根据数组建立一个完全二叉树,如下图
(代码中这一步无需操作,因为完全二叉树可以用一维数组来存储,初始数组默认就是一个完全二叉树)
第二步:把完全二叉树构建成大顶堆
从最后一个父节点开始,也就是从下标为3的7开始调整,因为7作为父节点,大于它的孩子节点5,所以不用调整;再看下标为2的3,因为3小于孩子节点,把最大的孩子节点8与3进行交换;然后再看下标为 1 的 33,它大于两个孩子节点,不用交换;再看下标为0的100,它大于孩子节点,不用交换;此时大顶堆已构建完成. 如图所示
这里需要注意:如果要构造完整的堆,要从下面往上构造,而且每次交换后,都要递归维护以"交换的那个孩子节点"为父节点的下面的堆,因为如果交换的节点非常小,那么可能小于它下面的堆,所以要递归向下维护堆,递归维护堆的函数在代码中是heapify方法(heapify方法后面会贴代码),heapify方法里边如果递归过程中父节点比某个孩子节点小,那么就交换位置,继续从被交换的那个孩子节点的位置往下递归维护堆;如果递归过程中父节点比两个孩子节点都大,不用交换,当然这个父节点肯定会大于孩子节点下边的堆,所以不需要继续递归下面的堆
第三步:取出最大值,并调整成为新堆 这时大顶堆的堆顶元素是最大值,把它与最后一个元素进行交换,然后从堆中拿走最后一个元素,也就是最大值,如下图
这时已经不满足堆的特点,只能说它是一个完全二叉树,所以此时要进行调整,注意调整堆和创建堆顺序不一样,调整堆应该从根节点开始调整,父节点和孩子节点交换后,下次递归调整的子堆就是以交换后的那个孩子为父节点的子堆,例如这一步,从下标为0的5开始调整,5和33交换后,下次递归就要调整下标为1的5为父节点的子堆,因为 5 比它的右孩子 11 大,所以交换位置(注意33 和 5 交换后,下标为2的8作为父节点的子堆不需要递归调整,因为交换后,以8为父节点的子堆没有动过,依然满足堆的特点)
此时已经调整好成为新堆,再把对顶元素和最后一个元素交换位置,即 33 和 3 交换,然后把取走33,如图
以此类推,中间的调整过程省略,最后的一次调整完如图所示
每一次交换完堆顶元素和最后一个元素后,也就是数组的arr[0]和 arr[n-i],例如第一次交换就是 arr[0]和 arr[n-1],第二次交换就是 arr[0]和 arr[n-2],以此类推,最后一次调整完成后,数组的元素就是升序排列.
注意
取出最大值后,调整堆时,如果要构造完整的堆,也要调用heapify方法递归维护下面的堆,但是这里说明一点,对于这里的堆排序来讲可以不用递归维护,因为只需要保证堆顶元素是最大的就可以,不必维护成为完整的堆.
堆排序完整代码
//8 堆排序
-(void)heapSort:(NSMutableArray*)arr{
//1. 构建大顶堆
[self buildHeap:arr];
int n = (int)arr.count;
//2. 依次取出最大值,然后继续调整堆
for (int i = n - 1; i>=0; i--) {
[self swapArray:arr index1:0 index2:i];//取出最大值放到数组的最后,也就是把堆顶的元素与最后一个交换
[self heapify:arr parent:0 n:i];//下次堆的个数逐渐减1,因为最大值取出后,就要从堆中拿走
}
}
/**
1,针对结点 i,将其两个子节点找出来,此三个结点构成一个最小单位的完全二叉树(越界的忽略)
2,找到这个最小单位的完全二叉树 的最大值,并将其交换至父节点的位置
3,递归调用,维护交换后 子节点与其子结点被破坏的堆关系,递归出口为叶节点
*/
-(void)heapify:(NSMutableArray*)arr parent:(int)i n:(int)n{
int c1 = i*2+1;//左孩子节点下标
int c2 = i*2+2;//右孩子节点下标
int max = i;
//找出三个节点中的最大值的下标,记录为max
if (c1 < n && [arr[c1] intValue] > [arr[max] intValue]) {
max = c1;
}
if (c2 < n && [arr[c2] intValue] > [arr[max] intValue]) {
max = c2;
}
//max != i,也就是最大值不是原来的父节点,需要交换位置,但是交换后,被交换位置的新元素可能比下面的堆还小,所以需要递归向下维护堆
if (max != i) {
[self swapArray:arr index1:max index2:i];
[self heapify:arr parent:max n:n];
}
}
-(void)buildHeap:(NSMutableArray*)arr{
int n = (int)arr.count;
//完全二叉树的下标与数组的下标一一对应,所以完全二叉树的最后一个节点的下标为last_node
int last_node = n - 1;
//最下面一个堆的父节点 根据求父节点的公式来的:p = (i-1)/2
int parent = (last_node-1)/2;
for (int i = parent; i>=0; i--) {
[self heapify:arr parent:i n:n];
}
}
堆排序的性能
堆排序的最好、最好和平均时间复杂度均为O(nlogn),空间复杂度是 O(1), 它是不稳定的排序