定义
我们先了解一下堆结构
堆是具有下⾯面性质的完全⼆二叉树:
- 每个结点的值都⼤大于或等于其左右孩⼦子结点的值, 称为⼤大顶堆;
- 或者每个结点的值都⼩小于等于其左右孩⼦子的结点的值,称为⼩小顶堆
原理
堆排序(Heap Sort)就是利用堆(假设我们选择⼤顶堆)进行排序的算法.它的基本思想:
- 1、将待排序的序列构成一个⼤顶堆,此时整个序列的最⼤值就堆顶的根结点,将它移走(其实就是将其与堆数组的末尾元素交换, 此时末尾元素就是最大值);
- 2、然后将剩余的n-1个序列列重新构成一个队,这样就会得到n个元素的次大值
- 3、如此重复执行,就能得到一个有序序列了
思路
堆排序思路路:
- 将无序序列构建成⼀个堆,根据升序降序需求选择大顶堆或小顶堆
- 将堆顶元素与末尾元素交换,将最大元素”沉"到数组末端;
- 重新调整结构,使其满⾜堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序;
总结:
- 先将待排数据构建成完全二叉树
- 再将二叉树处理成大堆树
- 将根结点和最末尾的元素进行替换,再将除了末尾元素外的树重新构建成大堆树,末尾元素不在参与重复构建大堆树
- 重新构建大堆树其实就是将交换的元素找到合适的位置,找到合适位置后就不用在处理,这样就减少了处理数据的次数
时间复杂度和空间复杂度
- 堆排序的时间复杂度为:O(nlogn)
- 堆排序是就地排序,空间复杂度为常数:O(1)
代码
//大顶堆调整函数;
/*
条件: 在L.r[s...m] 记录中除了下标s对应的关键字L.r[s]不符合大顶堆定义,其他均满足;
结果: 调整L.r[s]的关键字,使得L->r[s...m]这个范围内符合大顶堆定义.
*/
void HeapAjust(SqList *L,int s,int m){
int temp,j;
//① 将L->r[s] 存储到temp ,方便后面的交换过程;
temp = L->r[s];
//② j 为什么从2*s 开始进行循环,以及它的递增条件为什么是j*2
//因为这是颗完全二叉树,而s也是非叶子根结点. 所以它的左孩子一定是2*s,而右孩子则是2s+1;(二叉树性质5)
for (j = 2 * s; j <=m; j*=2) {
//③ 判断j是否是最后一个结点, 并且找到左右孩子中最大的结点;
//如果左孩子小于右孩子,那么j++; 否则不自增1. 因为它本身就比右孩子大;
if(j < m && L->r[j] < L->r[j+1])
++j;
//④ 比较当前的temp 是不是比较左右孩子大; 如果大则表示我们已经构建成大顶堆了;
/*
将无序的树构建成大顶堆时候先将下面的构建成,变量网上构建
*/
if(temp >= L->r[j])
break;
//⑤ 将L->[j] 的值赋值给j和j+1两个叶子节点的根结点
L->r[s] = L->r[j];
//⑥ 将s指向j; 因为此时L.r[4] = 60, L.r[8]=60. 那我们需要记录这8的索引信息.等退出循环时,能够把temp值30 覆盖到L.r[8] = 30. 这样才实现了30与60的交换;
//然后再对比较大的叶子的子树进行处理,让它成为大顶堆
s = j;
}
//⑦ 将L->r[s] = temp. 其实就是把L.r[8] = L.r[4] 进行交换;
L->r[s] = temp;
}
//10.堆排序--对顺序表进行堆排序
void HeapSort(SqList *L){
int i;
//1.将现在待排序的序列构建成一个大顶堆;
//将L构建成一个大顶堆;
//i为什么是从length/2.因为在对大顶堆的调整其实是从第一个非叶子的根结点开始进行调整,然后遍历往上排序
for(i=L->length/2; i>0;i--){
HeapAjust(L, i, L->length);
}
//2.逐步将每个最大的值根结点与末尾元素进行交换,并且再调整成大顶堆
for(i = L->length; i > 1; i--){
//① 将堆顶记录与当前未经排序子序列的最后一个记录进行交换;
swap(L, 1, i);
//② 将L->r[1...i-1]重新调整成大顶堆;
/*
因为从顶部开始调整的时候,只是原本树已经是大堆顶了,只是将根结点和最后一个元素进行了交换而已,所以再调整只是将最顶端的值调整到合适位置,这样就减少了遍历
*/
HeapAjust(L, 1, i-1);
}
}