堆排序,平均时间复杂度为O(nlogn),是一种不稳定的排序算法。堆排序利用 堆 这种数据结构来对数组进行排序,堆是一颗完全二叉树在数组中的表示,其任意一个节点的值大于等于它的父节点,但实际上它是一个数组,只是可以当成是一个完全二叉树:
假设有一个数组为 -1,1,0,它是从 0 开始编号的,而当它任意一结点的值都大于等于其父节点是,它就是一个堆了,比如 A = {-1,1,0} 或者 B = {-1,1,0}这两个序列,写成完全二叉树为(数组从左至右其实是二叉树的层序遍历):
*** -1(0) ************************************ -1(0) *****************
1(1) 0(2) 和********************************0(1) 1(2)
其中,括号内的数字是这个元素在完全二叉树中的编号,同时也是它在数组中的下标,上述两个数组列成二叉树可以发现,任意结点的值都大于它的父节点(父节点存在的话),而当一个数组构成了一个堆的话,其根结点(也就是下标为0的点)必定是最小值,此时将他与最后一个元素交换,然后将最后一个元素剔除,剩余元素再重新构成一个堆,之后把新的堆的根结点与现在最后一个元素交换,如此循环直到不能交换,此时的数组就变成了降序排列:(以 -1,1,0为例)这是一个小顶堆(根节点的值最小),在原数组中是 {-1,1,0},然后将根节点与最后一个元素交换得到 {0,1,-1},剩余两个元素 0 和 1,再构建一个小顶堆为:
*******0(0)
1(1)
在数组中就是{0,1,-1},然后将根节点与堆中的最后一个元素交换得到{1,0,-1},排序完成对于一个已经完成的小顶堆来说,将根结点与最后一个元素交换肯定会打破这个小顶堆,因此,需要重新修复。
对于一个已经是小顶堆的数组 A = {-5,-3,-2,1,3,4,5},他构成的堆如图
将根结点 -5 与最后一个结点 5 交换之后变为 A = {5,-3,-2,1,3,4,-5},此时 -5 将被‘剔出’数组,即所有操作都不涉及它。要对根结点修复,只需比较 5,-3, -2三个结点的大小,将最小的 -3 作为新的根节点,此时数组变为 A = {-3,5,-2,1,3,4,-5},如下图
此时可以发现,没有移动那段分支还是符合小顶堆的规则(-5 已被剔除),只有交换了的那一边才不再满足小顶堆了,因此还需要将左边分支继续修复,如下
则数组变为 A = {-3,1,-2,5,2,4,-5},满足小顶堆的条件,再将根结点 -3 跟最后一个结点交换,数组变为A = {4,1,-2,5,2,-3,-5},一直重复即可将这一数组变得有序不过这一切的前提都是给定的数组原本就是一个小顶堆,如果不是这样的话,就需要先构建一个完整的小顶堆
假设给定的数组为 1, -1, 2, 5, -4, 0, -2, -3, 3, 4,它的完全二叉树表示为下图
这是一个完全无序的二叉树,要升序排列,首先要构建大顶堆(上面构建的小顶堆对应为降序排列),同样的,对于一个三个节点的二叉树来说,两两比较三个节点的元素值大小,将最大的交换到父节点处,从最后一个结点开始构建堆,上面说了,构建完一颗子树后交换的那一边的大顶堆也被破坏了,要修复,但没有交换的那一边不需要动,流程如下:
- 从最后一个结点 4 开始构建,观察二叉树发现,最后一个结点所在的子树只有 2 个节点,因此就比较这两个的值,4 > -4,将 4 放到父节点的位置,则这颗子树左边 交换了,但是它没有子节点,因此不用修复,同样的,对于 5,-3,3 和 2,0,-2 这两棵子树也是构建完不用修复,构建好的数组为
1, -1, 2, 5, 4, 0, -2, -3, 3, -4,只将 -4 与 4 交换了,其构建的二叉树如下图
然后开始构建 -1,5,4 这颗子树,比较后将 5 作为父节点,即 -1 与 5 交换,如下图
交换完之后,交换的那一边需要修复堆,因为交换完有可能会改变大顶堆的结构,这里明显 -1,-3,3 这三个数中 -1 不是最大的,因此修复后应该将 3 作为父节点,同时交换的那一边由于没有子节点,因此不需要再往下修复
了,修复后得到的数组为 1,5,2,3,4,0,-2,-3,-1,-4,二叉树结构如下图
这样一直到根结点也构建好堆,得到一个大顶堆数组为 5,4,2,3,1,0,-2,-3,-1,-4,大顶堆如下图
这样,就由一个完全无序的数组构建好了一个大顶堆,然后按开头讲的方式,将根结点与最后一个结点交换,重新构建大顶堆,再交换,直到所有的结点都被交换完,排序就结束了
这也是一个分治递归的思想,先将一个大问题分解,考虑一个个小的问题,然后慢慢的解决直到将大问题解决完。值得注意的是,由于这是一颗完全二叉树,因此通过父节点找其子节点或是通过子节点找父节点都是很简单的。根据上述思路可以写出堆排序升序算法(构建大顶堆)
/**
* 堆排序需要将根结点元素与最后一个元素交换,
* 因此封装一个交换函数供多次调用
* @param nums 待交换元素的数组
* @param index1 要交换的第一个元素的下标
* @param index2 要交换的第二个元素的下标
*/
void swap(int nums[], int index1, int index2) {
int tmp = nums[index1];
nums[index1] = nums[index2];
nums[index2] = tmp;
}
/******************************************** 大顶堆 ************************************/
/**
* 将编号为 number 的子树构建成堆
* @param nums 需要构建成堆的数组
* @param numsSize 当前数组可操作的数量,不包括被 '剔除' 的元素
* @param number 要构建堆的子树的父节点
*/
void heapfiyUp(int nums[], int numsSize, int number) {
/**
* number 为父节点编号,由于整个堆的根结点是与数组下标一一对应的,
* 即 根结点的编号为 0,那么每个父节点对应的子节点 (如果有的话)的编号为
* 2*number + 1,2*number + 2,一个结点如果存在子节点,那么它子节点
* 的编号一定小于等于数组的最大编号 numsSize - 1
*/
int child1 = 2*number + 1;
int child2 = 2*number + 2;
int max = number;
/**
* 找到三个节点的最大值,如果当前 number 所在的元素不是最大,
* 说明此时的父节点是经过交换得到的,那么交换的那一边应该修复
* 大顶堆,如果 number 就是最大的,说明没有交换,直接返回
*/
if(child1<numsSize && nums[child1]>nums[max]) max = child1;
if(child2<numsSize && nums[child2]>nums[max]) max = child2;
if(max != number) {
/**
* max 保存的是三个数中最大的元素的下标,先将其与 number 交换,
* 交换完后 max 所处的就是需要修复的那一边,因此直接把 max 修复完
*/
swap(nums, number, max);
heapfiyUp(nums, numsSize, max);
}
}
/**
* 从最后一个父结点开始构建大顶堆
* @param nums 待排序数组
* @param numsSize 数组长度
*/
void buildUpHeap(int nums[], int numsSize) {
/**
* 一个节点 number 的两个子节点为
* c1 = 2*number + 1,
* c2 = 2*number + 2,
* 若已知一个子节点 c1 或 c2,反推父节点为
* parent = (c1-1) / 2,或
* parent = (c2-2) / 2,
* 由于构建的是完全二叉树,因此 c2 的编号一定大于 c1,
* 并且 c1 的编号一定是奇数,c2 为偶数,偶数减 1 和减 2
* 然后除以 2 得到的结果是一样的,因此可以得出:
* parent = (number-1) / 2
* 要从最后一个父节点开始,即要找到最后一个结点的付父节点,
* 最后一个结点的编号为 numsSize - 1,因此得到计算最后一个父节点
* 的计算公式:
* parent = (numsSize-2) / 2
*/
int parent = (numsSize-2) / 2;
/**
* 从 parent 开始构建大顶堆
*/
for(int i = parent; i >= 0; --i) heapfiyUp(nums, numsSize, i);
}
/**
* 交换大顶堆的根结点和最后一个结点,修复堆之后重复
* @param nums 待排序数组
* @param numsSize 数组长度
*/
void heapUpSort(int nums[], int numsSize) {
/**
* 构建大顶堆
*/
buildUpHeap(nums, numsSize);
/**
* 将根结点 (编号为 0) 与 末尾节点 i 交换
*/
for(int i = numsSize-1; i >= 0; --i) {
swap(nums, 0, i);
heapfiyUp(nums, i, 0);
}
}
将构建的堆变为小顶堆即可得到降序算法,顺便还实现了一下非递归修复大顶堆
源代码
#include <stdio.h>
/**
* 堆排序需要将根结点元素与最后一个元素交换,
* 因此封装一个交换函数供多次调用
* @param nums 待交换元素的数组
* @param index1 要交换的第一个元素的下标
* @param index2 要交换的第二个元素的下标
*/
void swap(int nums[], int index1, int index2) {
int tmp = nums[index1];
nums[index1] = nums[index2];
nums[index2] = tmp;
}
/******************************************** 大顶堆 ************************************/
#if 1
/**
* 将编号为 number 的子树构建成堆
* @param nums 需要构建成堆的数组
* @param numsSize 当前数组可操作的数量,不包括被 '剔除' 的元素
* @param number 要构建堆的子树的父节点编号
*/
void heapfiyUp(int nums[], int numsSize, int number) {
/**
* number 为父节点编号,由于整个堆的根结点是与数组下标一一对应的,
* 即 根结点的编号为 0,那么每个父节点对应的子节点 (如果有的话)的编号为
* 2*number + 1,2*number + 2,一个结点如果存在子节点,那么它子节点
* 的编号一定小于等于数组的最大编号 numsSize - 1
*/
int child1 = 2*number + 1;
int child2 = 2*number + 2;
int max = number;
/**
* 找到三个节点的最大值,如果当前 number 所在的元素不是最大,
* 说明此时的父节点是经过交换得到的,那么交换的那一边应该修复
* 大顶堆,如果 number 就是最大的,说明没有交换,直接返回
*/
if(child1<numsSize && nums[child1]>nums[max]) max = child1;
if(child2<numsSize && nums[child2]>nums[max]) max = child2;
if(max != number) {
/**
* min 保存的是三个数中最大的元素的下标,先将其与 number 交换,
* 交换完后 min 所处的就是需要修复的那一边,因此直接把 min 修复完
*/
swap(nums, number, max);
heapfiyUp(nums, numsSize, max);
}
}
#endif
/**此条件改为 1 且上面递归的 #if 条件改为 0 即可运行非递归方法,
* 需要将上面递归形式的 #if 条件改为 0,不能两个都是 0 或 1,会报错
*/
#if 0
/**
* 非递归构建大顶堆
* @param nums 待排序数组
* @param numsSize 当前数组可操作的数量,不包括被 '剔除' 的元素
* @param index 要构建堆的子树的父节点编号
*/
void heapfiyUp(int nums[], int numsSize, int index) {
int i = index;
while(1) {
/**
* 找到两个子节点
*/
int c1 = 2*i + 1;
int c2 = 2*i + 2;
/**
* 找到三个节点中最大值
*/
int max = i;
if(c1<numsSize && nums[c1]>nums[max]) max = c1;
if(c2<numsSize && nums[c2]>nums[max]) max = c2;
/**
* 最大值不等于 i 则说明发生了交换,
* 继续修复;最大值等于 i ,没有发生
* 交换,直接退出
*/
if(max != i) {
swap(nums, i, max);
i = max;
} else break;
}
}
#endif
/**
* 从最后一个父结点开始构建大顶堆
* @param nums 待排序数组
* @param numsSize 数组长度
*/
void buildUpHeap(int nums[], int numsSize) {
/**
* 一个节点 number 的两个子节点为
* c1 = 2*number + 1,
* c2 = 2*number + 2,
* 若已知一个子节点 c1 或 c2,反推父节点为
* parent = (c1-1) / 2,或
* parent = (c2-2) / 2,
* 由于构建的是完全二叉树,因此 c2 的编号一定大于 c1,
* 并且 c1 的编号一定是奇数,c2 为偶数,偶数减 1 和减 2
* 然后除以 2 得到的结果是一样的,因此可以得出:
* parent = (number-1) / 2
* 要从最后一个父节点开始,即要找到最后一个结点的付父节点,
* 最后一个结点的编号为 numsSize - 1,因此得到计算最后一个父节点
* 的计算公式:
* parent = (numsSize-2) / 2
*/
int parent = (numsSize-2) / 2;
/**
* 从 parent 开始构建大顶堆
*/
for(int i = parent; i >= 0; --i) heapfiyUp(nums, numsSize, i);
}
/**
* 交换大顶堆的根结点和最后一个结点,修复堆之后重复
* @param nums 待排序数组
* @param numsSize 数组长度
*/
void heapUpSort(int nums[], int numsSize) {
/**
* 构建大顶堆
*/
buildUpHeap(nums, numsSize);
/**
* 将根结点 (编号为 0) 与 末尾节点 i 交换
*/
for(int i = numsSize-1; i >= 0; --i) {
swap(nums, 0, i);
heapfiyUp(nums, i, 0);
}
}
/******************************************** 小顶堆 ************************************/
/**
* 将编号为 number 的子树构建成堆
* @param nums 需要构建成堆的数组
* @param numsSize 当前数组可操作的数量,不包括被 '剔除' 的元素
* @param number 要构建堆的子树的父节点
*/
void heapfiyDown(int nums[], int numsSize, int number) {
/**
* number 为父节点编号,由于整个堆的根结点是与数组下标一一对应的,
* 即 根结点的编号为 0,那么每个父节点对应的子节点 (如果有的话)的编号为
* 2*number + 1,2*number + 2,一个结点如果存在子节点,那么它子节点
* 的编号一定小于等于数组的最大编号 numsSize - 1
*/
int child1 = 2*number + 1;
int child2 = 2*number + 2;
int min = number;
/**
* 找到三个节点的最小值,如果当前 number 所在的元素不是最大,
* 说明此时的父节点是经过交换得到的,那么交换的那一边应该修复
* 大顶堆,如果 number 就是最大的,说明没有交换,直接返回
*/
if(child1<numsSize && nums[child1]<nums[min]) min = child1;
if(child2<numsSize && nums[child2]<nums[min]) min = child2;
if(min != number) {
/**
* max 保存的是三个数中最大的元素的下标,先将其与 number 交换,
* 交换完后 max 所处的就是需要修复的那一边,因此直接把 max 修复完
*/
swap(nums, number, min);
heapfiyDown(nums, numsSize, min);
}
}
/**
* 从最后一个父结点开始构建小顶堆
* @param nums 待排序数组
* @param numsSize 数组长度
*/
void buildDownHeap(int nums[], int numsSize) {
/**
* 一个节点 number 的两个子节点为
* c1 = 2*number + 1,
* c2 = 2*number + 2,
* 若已知一个子节点 c1 或 c2,反推父节点为
* parent = (c1-1) / 2,或
* parent = (c2-2) / 2,
* 由于构建的是完全二叉树,因此 c2 的编号一定大于 c1,
* 并且 c1 的编号一定是奇数,c2 为偶数,偶数减 1 和减 2
* 然后除以 2 得到的结果是一样的,因此可以得出:
* parent = (number-1) / 2
* 要从最后一个父节点开始,即要找到最后一个结点的付父节点,
* 最后一个结点的编号为 numsSize - 1,因此得到计算最后一个父节点
* 的计算公式:
* parent = (numsSize-2) / 2
*/
int parent = (numsSize-2) / 2;
/**
* 从 parent 开始构建大顶堆
*/
for(int i = parent; i >= 0; --i) heapfiyDown(nums, numsSize, i);
}
/**
* 交换小顶堆的根结点和最后一个结点,修复堆之后重复
* @param nums 待排序数组
* @param numsSize 数组长度
*/
void heapDownSort(int nums[], int numsSize) {
/**
* 构建大顶堆
*/
buildDownHeap(nums, numsSize);
/**
* 将根结点 (编号为 0) 与 末尾节点 i 交换
*/
for(int i = numsSize-1; i >= 0; --i) {
swap(nums, 0, i);
heapfiyDown(nums, i, 0);
}
}
int main() {
int arr1[] = {1, -1, 2, 5, -4, 0, -2, -3, 3, 4};
int len1 = sizeof(arr1) / sizeof(arr1[0]);
heapUpSort(arr1, len1);
int arr2[] = {1, -1, 2, 5, -4, 0, -2, -3, 3, 4};
int len2 = sizeof(arr2) / sizeof(arr2[0]);
heapDownSort(arr2, len2);
printf("UP:\t");
for(int i = 0; i < len1; ++i) printf("%d ", arr1[i]);
printf("\nDOWN:\t");
for(int i = 0; i < len2; ++i) printf("%d ", arr2[i]);
return 0;
}