您的位置:首页 > 其它

【排序算法】堆排序的分析

2018-03-24 16:30 288 查看
在分析堆排序之前,我先讲一讲什么是堆
如果一棵树中每个节点的关键字都大于或等于所有子节点中的关键字(子节点存在),那么就称树是堆有序的,在一棵堆有序的树中,不存在关键字大于根节点关键字的节点,那么什么是堆呢?
堆是一个节点的集合,表现形式为数组,其中关键字按照堆有序的完全二叉树的形式排列



如上图,上图是一个完全二叉树,而且每个关键字都大于他的任意一个子节点(子节点存在),把这棵树放到数组里面,如下:arr[] = {X, T, O, G,S,M, N, A, E, R, A, I}放到数组里面,位置i处的子节点是2*i + 1和2*i + 2,N个节点的完全二叉树中的每条路径越有lgN个节点
堆分为大根堆和小根堆,顾名思义,大根堆就是树中根节点是最大的关键字的堆,小根堆就是根节点是较小的根节点的堆,上面的堆是大根堆,所以我们来讨论一下大根堆
那么如何来构造一个大根堆呢?
两种方法:

1.自底向上堆化

假设有一个大根堆,它的某个个节点的关键字大于那个节点的父节点的关键字,堆的性质受到侵犯,那么我们可以交换该节点和父节点,交换之后,节点大于它的两个子节点,但是仍然可能大于它的父节点,所以我们用同样的方法去修正,直到遇到一个较大的父节点或者到达根节点
如图:




实现的代码如下://重新使大堆有序
void FixUp(int arr[], int right) {
assert(arr);
//只有一个元素的数组不必进行判断
assert(right > 0);
int i = right - 1;
while (i >= 0) {
int j = (i - 1) / 2;
if (arr[i] > arr[j]) {
Swap2(&arr[i], &arr[j]);
i = j;
}
else {
break;
}
}
}那么,有了这个方法,我们就可以每次给数组后面添加一个元素的方法来使一个无序的数组称为一个堆有序的数组,构造一个大堆的代码如下:void Create_Heap(int arr[], int right) {
assert(arr);
assert(right > 1);
int i = 0;
for (i = 1; i <= right; ++i) {
FixUp(arr, i);
}
}

2.自顶向下堆化

第二种方法是自顶向下堆化,还是假设有一个大根堆,它的某个节点的关键字小于它的某个节点,堆得性质收到侵犯,我们就用同样的思路,交换该节点和它的较大的子节点,然后再继续这样的操作,直到它大于它的两个子节点或者到达了叶节点
如图:



调整堆的代码如下:void AdjustDown(int arr[], int left, int right) {
//调整堆,把堆顶元素往下调整,直到重新变成堆有序
//向下调整大顶堆,每次应该和两个子节点中较大的比较,如果比大的子节点小就交换
int i = left;
while (1) {
int j = i * 2 + 1;
if (j >= right) {
return;
}
//注意这里一定要判断j + 1的合法性
if (j + 1 < right && arr[j] < arr[j + 1]) {
//让j成为较大元素的下标
j = j + 1;
}
if (arr[i] < arr[j]) {
Swap(&arr[i], &arr[j]);
i = j;
}
else {
break;
}
}//循环结束
}//函数结束
调整大根堆是要比较当前节点和子节点中较大的节点,而如果调整小根堆的话,就需要和子节点中较小的节点比较
有了上面的代码,我们也可以很容易去把一个无序的数组构造成一个大根堆,代码如下:void AdjustDown(int arr[], int left, int right)

//调整堆,把堆顶元素往下调整,直到重新变成堆有序
//向下调整大顶堆,每次应该和两个子节点中较大的比较,如果比大的子节点小就交换
int i = left;
while (1) {
int j = i * 2 + 1;
if (j >= right) {
return;
}
//注意这里一定要判断j + 1的合法性
if (j + 1 < right && arr[j] < arr[j + 1]) {
//让j成为较大元素的下标
j = j + 1;
}
if (arr[i] < arr[j]) {
Swap(&arr[i], &arr[j]);
i = j;
}
else {
break;
}
}//循环结束
}//函数结束

void Greate_Heap2(int arr[], int right) {
int i = 1;
for (; i < right; ++i) {
AdjustDown(arr, 0, i);
}
好了,我们已经搞懂了如何去把一个数组通过两种方法构造成一个堆,接下来就看看使用堆如何对数据进行排序
对于一个大根堆而言,它的根节点的关键字肯定是最大的,所以,我们每次取出根节点的元素并输出,再把最后一个元素放到根节点,然后再调整堆使堆重新有序,最后就把堆中的元素逆序输出了
如图:



绿色的线代表不在堆中的元素,最后第四个图我们可以看到,按照层序遍历这棵树,得到的序列就是堆中元素的递增序列
实现的代码如下:void AdjustDown(int arr[], int left, int right) {
//调整堆,把堆顶元素往下调整,直到重新变成堆有序
//向下调整大顶堆,每次应该和两个子节点中较大的比较,如果比大的子节点小就交换
int i = left;
while (1) {
int j = i * 2 + 1;
if (j >= right) {
return;
}
//注意这里一定要判断j + 1的合法性
if (j + 1 < right && arr[j] < arr[j + 1]) {
//让j成为较大元素的下标
j = j + 1;
}
if (arr[i] < arr[j]) {
Swap(&arr[i], &arr[j]);
i = j;
}
else {
break;
}
}//循环结束
}//函数结束

void _HeapSort(int arr[], int left, int right) {
//先构造一个大顶堆
int i = (right - 1 - 1) / 2;
for (; i >= left; --i) {
AdjustDown(arr, i, right - 1);
}
//再把堆顶元素放入到堆得末尾,然后再把堆重新构造
int j = right - 1;
for (; j >= left; j--) {
Swap(&arr[left], &arr[j]);
//这个区间也是前闭后开区间
AdjustDown(arr, left, j);
}
}

void HeapSort(int arr[], int size) {
assert(arr);
if (size <= 1) {
return;
}
_HeapSort(arr, 0, size);
}堆排序的时间复杂度是O(NlogN),它是一种不稳定的排序
对N个元素排序,使用堆排序不超过2NlgN次比较
通过对堆的性质分析,我们发现,与快速排序相比较,堆的排序需要的时间很稳定,快速排序在处理具有相同元素的时候可能会很慢,而堆排序则不会,反而在处理随机文件的时候,堆排序可能会比快速排序慢
而相比归并排序,堆排序又不是稳定排序,但是归并排序需要占用额外的空间,而堆排序可以进行原位排序,不需要大量的空间
好了对堆排序就总结到这里,对数据进行分析选择排序方法是很有必要的~
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  堆排序