堆排序
1. 基本概念
堆,分为大顶堆(大堆)和小顶堆(小堆),是顺序存储的完全二叉树,并且满足以下特性之一:
(1) 任意非终端结点关键字不小于左右子结点(大堆)
ki >= k2i+1并且ki>=k2i+2 其中,0 <= i <= (n-1)/2,n是数组元素个数
(2) 任意非终端结点关键字不大于左右子结点(小堆)
ki <= k2i+1并且ki<=k2i+2 其中,0 <= i <= (n-1)/2,n是数组元素个数
调整(也有叫筛选):
从当前结点(要求是非终端结点)开始,
对于大堆,要求当前结点关键字不小于子结点,如不符合,则将最大的子结点与当前结点交换。循环迭代交换后的子树,确保所有子树都符合大堆特性。
小堆调整过程类似。
2. 基本思想
堆排序就是利用构建堆和输出堆顶元素的过程,不断对堆进行调整以保证当前结点及其孩子结点满足堆特性,从而达到对初始数组元素进行排序的目的。
大堆通常对应升序序列,小堆通常对应降序排列。
核心步骤:
1) 构建堆(大堆/小堆)
从最后一个非终端结点开始,向前进行调整,保证当前结点及其子树符合堆特性;
2) 输出有序序列
交换堆顶与末尾叶子结点,堆顶输出到数组的有序序列末尾,而不参与堆的调整。从交换后的堆顶开始调整,以确保当前结点及其子树符合堆特性。
3. 实例
下面举个例子,利用小堆进行降序排列。
初始序列 |
49 |
38 |
65 |
97 |
76 |
13 |
27 |
49‘ |
位置 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
说明:
|
3.1. 构建堆
1) 初始序列对应初始堆
从最后一个非叶子结点开始,向前进行调整,确保符合特性
最后一个非叶子结点位置:(n-1) / 2 = 3, n=8
总共调整次数:(n-1)/2 +1 = 4
第1次调整:选择最后一个非叶子结点元素为97(位置3)为当前父结点,与其子结点进行比较,选择最小的结点作为当前父结点。
第1次调整后序列 |
49 |
38 |
65 |
49’ |
76 |
13 |
27 |
97 |
位置 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
第2次调整:选择上一次结点的前一个结点65(位置2)为当前结点进行调整。
第2次调整后序列 |
49 |
38 |
13 |
49’ |
76 |
65 |
27 |
97 |
位置 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
第3次调整:选择上一次结点的前一个结点38(位置1)为当前结点进行调整。
第3次调整后序列 |
49 |
38 |
13 |
49’ |
76 |
65 |
27 |
97 |
位置 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
第4次调整:选择上一次结点的前一个结点49(位置0)为当前结点进行调整。
第4次调整后序列 |
13 |
38 |
27 |
49’ |
76 |
65 |
49 |
97 |
位置 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
3.2. 输出堆顶元素
将已经构建好的小堆,输出堆顶元素,和末尾元素交换,相当于堆顶移动到数组末尾形成有序序列,未排序元素移动到堆顶。从新的堆顶开始进行调整,直到堆重新符合小堆特性。
交换堆顶和末尾(堆的末尾,不包括已经排好序的部分),并将交换后的堆末尾作为有序序列的一部分,而不再属于堆。
一次交换后,发现97新的位置比子结点大,需要继续调整。
..
这样,不断输出所有堆顶到数组末尾,最终可以得到
有序序列 |
97 |
76 |
65 |
49 |
49’ |
38 |
27 |
13 |
位置 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
4. 实现代码
4.1. 核心算法代码
1 /** 2 * 小堆 <=> 升序排列 3 * 从最后一个非叶子结点开始, 向前进行调整 4 * @param a 待排序序列(数组) 5 * @param n 待排序元素个数 6 */ 7 void HeapSort(int a[], int n) 8 { 9 // S1 建堆 10 // 从最后一个非叶子结点开始, 向前进行调整 11 for(int i = LocOfLastNonLeaf(a, n); i >= 0; i --) 12 {// LocOfLastNonLeaf : (n-1) / 2 13 HeapAdjust(a, i, n); 14 } 15 16 // S2 输出并调整 17 for(int j = n-1; j > 0; j --) 18 {// 判断条件不用加"=", 因为j=0时等价于数组只有一个元素, 即只有一个根节点, 而无子树 19 Swap(a[0], a[j]); // 形参为同名参数, 直接交换a[0]和a[j] 20 HeapAdjust(a, 0, j); 21 } 22 }
1 /** 2 * 筛选位置i, 调整堆 3 * @param a 待排序序列(数组) 4 * @param i 筛选位置 5 * @param len 数组元素个数 6 */ 7 void HeapAdjust(int a[], int i, int n) 8 { 9 if(i > n / 2 - 1) 10 {// 叶子结点, 无子树 11 return; 12 } 13 14 // 检查结点i是否符合小堆特性, 如果不符合, 需要与最小子结点交换 15 for(int k = 2*i +1; k < n; k = 2*k + 1) 16 { 17 // 判断右子树是否比左子树更小 18 if(k+1 < n && a[k+1] < a[k]) 19 { 20 k ++; // 更新最小子结点 21 } 22 23 if(a[i] > a[k]) 24 { 25 Swap(a[i], a[k]); // 与最小的子结点交换 26 i = k; // 将左子结点设为当前结点 27 } 28 else 29 {// 符合小堆特性 30 break; 31 } 32 } 33 }
4.2. 调用
1 int main() 2 { 3 int a[] = { 4 // 10,2,5,9,5,55,21,33,15 5 49, 38, 65, 97, 76, 13, 27, 49 6 }; 7 int a_len = sizeof(a) / sizeof(a[0]); 8 9 printf("原始序列: "); 10 PrintArrary(a, a_len); // 打印数组 11 12 HeapSort(a, a_len); // 堆排序 13 14 printf("小堆排序后序列(降序): "); 15 PrintArrary(a, a_len); // 打印数组 16 17 return 0; 18 }
4.3. 测试结果