1. 想要理解堆排序,首先就要了解完全二叉树,因为堆排序就是在完全二叉树的基础上进行操作的。假设存在一棵完全二叉树(图中没画完整,用省略号替代了该有的结点),有 k+1 层(从第 0 层算起):
其实完全可以不在意二叉树内的编号是多少,编号是从 0 开始还是从 1 开始,亦或者从 2,3 开始,这都不重要。只需记住一点:每层父亲与下一层的左儿子相差多少。以上图最左边为例,其实每层父亲与左儿子的差值就是父亲那层节点的总数。记住这一点,就容易理解多了,假如父亲节点为 i,且父亲那层节点总数为 n,那么左儿子的节点自然就为 i+n。如果节点 i 是在第 0 层,那么完全二叉树的节点通式就可以写成:
编号从 0 开始,左儿子节点都是 2i+1;编号从 1 开始,左儿子节点都是 2i;编号从 2 开始,左儿子节点都是 2i-1。为什么会是这样呢?根据不同的 i 取值,将父亲节点*2,找其与左儿子节点值的关系。(感觉这里应该有个更加强大的通式,而不是因为 i 的不同取值导致的节点关系不同,待定)
2. 堆,在逻辑上是完全二叉树,但是物理上其实就是数组。此外,堆排序有两个准备工作,一是插入堆,二是调整堆,最后才能完成堆排序。
插入堆:给定一个大根堆,传入待插入元素的下标,将这个元素插入到已有的大根堆中,形成新的大根堆。
插入过程中最有意思的点,是找到插入元素的父节点。正常情况下,找其父节点肯定要判断 index 是左孩子还是右孩子。在编号从 0 开始的条件下,左孩子的公式是 2*i+1(假设父节点下标为 i),右孩子公式为 2*i+2。反推便是,若孩子下标 index 为左孩子,则父节点为 (index-1)/2;若孩子下标 index 代表右孩子,父节点则为 (index-2)/2。但在实际编程中,这两个可以用一句代码进行代替,即 farther_index = (index-1)/2; 这样做的原因是:如果 index 为左孩子,正好求其父节点;如果 index 是右孩子,那么 index 一定是偶数(2i+2),index-1 便是奇数,(index-1)/2 在计算机中执行时,奇数产生的 .5 会自动被抹去,相当于 (index-2)/2,所以对于右孩子,在计算机中执行 (index-1)/2 就是求父节点。
/*
* 建立大根堆,是由底向上建立的
* *array: 大根堆首地址
* index:插入数的下标,默认插入数先放在了大根堆的最后一位
*/
void HeapInsert(int *array, int index){
if(index == 0){
return; // 不用管,因为就它一个结点,一定是大根堆
}
while(*(array+index) > *(array+(index-1)/2)){ // 儿子大于父亲
SWAP(*(array+index), *(array+(index-1)/2), int);
index = (index-1)/2; // 交换之后儿子变父亲,继续向上对比
}
}
看上面代码可以发现,插入堆的过程实际上自底向上的过程。
调整堆:给定一个堆,从传入的 index 节点开始,自顶向下逐渐调整,形成大根堆。注意这里有个隐含的东西,index 节点之前仍旧保持大根堆,index 节点之后大根堆才被破坏。
/*
* 调整大根堆,是自顶向下调整的
* *array: 大根堆首地址
* index:从该节点开始,向下调整大根堆
* heap_size:大根堆的元素个数
*/
void Heapify(int *array,int index,int heap_size){
while(2*index+1 < heap_size){ // 只要左孩子还在
int index_max = 2*index+1;
if(2*index+2 < heap_size){ // 如果右孩子在,先比较左右孩子
index_max = *(array+2*index+1) > *(array+2*index+2)?(2*index+1):(2*index+2);
}
if(*(array+index) < *(array+index_max)){ // 再把较大的孩子与父亲进行比较
SWAP(*(array+index), *(array+index_max), int);
index = index_max;
}
else{
return; // 如果父节点大,那么后续都不用换了
}
}
}
堆排序:堆排序实际上就是在建立大根堆的基础上,将堆顶的元素弹出,然后堆的大小递减,再重新调整堆,形成了一个新的大根堆。之后重复之前的步骤,弹出堆顶元素,直到大根堆只有一个元素为止,这样的一个循环过程。
大根堆的堆顶实质是就是数组的 0 号元素,也是堆中元素的最大值。弹出堆顶元素其实就是堆顶元素和大根堆的末位置元素进行互换。
void HeapSort(int *array, int length){
// 1.先建立大根堆
for(int i=0;i<length;i++){
HeapInsert(array, i);
}
int heap_size = length;
while(heap_size > 1){
SWAP(*(array), *(array+heap_size-1), int); // 2. 弹出堆顶元素
heap_size--; // 弹出之后,堆元素个数更新
Heapify(array, 0, heap_size); // 3. 调整堆
}
}
完整代码:
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#define SWAP(a,b,type) do{type temp=a;a=b;b=temp;}while(0)
void Generate(int **array, int length, int upper_bound){
*array = malloc(sizeof(int)*length);
if(*array){
for(int i=0;i<length;i++){
*(*array+i) = rand() % upper_bound;
}
}else{
printf("Error!");
}
}
/*
* 建立大根堆,是由底向上建立的
* *array: 大根堆首地址
* index:插入数的下标,默认插入数先放在了大根堆的最后一位
*/
void HeapInsert(int *array, int index){
while(*(array+index) > *(array+(index-1)/2)){ // 儿子大于父亲
SWAP(*(array+index), *(array+(index-1)/2), int);
index = (index-1)/2; // 交换之后儿子变父亲,继续向上对比
}
}
/*
* 调整大根堆,是自顶向下调整的
* *array: 大根堆首地址
* index:从该节点开始,向下调整大根堆
* heap_size:大根堆的元素个数
*/
void Heapify(int *array,int index,int heap_size){
while(2*index+1 < heap_size){ // 只要左孩子还在
int index_max = 2*index+1;
if(2*index+2 < heap_size){ // 如果右孩子在,先比较左右孩子
index_max = *(array+2*index+1) > *(array+2*index+2)?(2*index+1):(2*index+2);
}
if(*(array+index) < *(array+index_max)){ // 再把较大的孩子与父亲进行比较
SWAP(*(array+index), *(array+index_max), int);
index = index_max;
}
else{
return; // 如果父节点大,那么后续都不用换了
}
}
}
void HeapSort(int *array, int length){
// 1.先建立大根堆
for(int i=0;i<length;i++){
HeapInsert(array, i);
}
int heap_size = length;
while(heap_size > 1){
SWAP(*(array), *(array+heap_size-1), int); // 2. 弹出堆顶元素
heap_size--; // 弹出之后,堆元素个数更新
Heapify(array, 0, heap_size); // 3. 调整堆
}
}
int main()
{
srand(time(NULL));
int *array;
int length = 10;
int upper_bound = 10;
Generate(&array, length, upper_bound);
printf("初始数组:\n");
for(int i=0;i<length;i++){
printf("%d ", *(array+i));
}
for(int i=0;i<length;i++){
HeapInsert(array, i);
}
printf("\n大根堆:\n");
for(int i=0;i<length;i++){
printf("%d ", *(array+i));
}
printf("\n弹出最大值后的大根堆:\n");
SWAP(*(array), *(array+length-1), int);
for(int i=0;i<length;i++){
printf("%d ", *(array+i));
}
printf("\n重新调整后的大根堆:\n");
Heapify(array, 0, 9);
for(int i=0;i<length;i++){
printf("%d ", *(array+i));
}
HeapSort(array, length);
printf("\n堆排序之后的数组:\n");
for(int i=0;i<length;i++){
printf("%d ", *(array+i));
}
free(array);
return 0;
}