C语言堆排序(HeapSort)的思想和代码实现
经过一晚上和有一早上的思考和学习,在Clion上反复的单步调试之后,我总结了关于堆排序这个算法的一点体会。现在来记录一下,如有错误,欢迎批评指出,谢谢!
首先:什么是堆排序,为什么叫堆?
Heapsort是一种根据选择排序的思想,利用堆这种数据结构 所设计的一种排序算法
选择排序的思想是什么?:每一趟比较找到这个序列中的最值,拿出来和最前面的元素交换,交换完之后,这个序列从前面开始减去一个(因为前面放的是最值,不需要放在序列里再次比较)
那么这里的堆是什么意思呢?:堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
什么是完全二叉树?即,每个节点都一一有序对应的满二叉树,如下图所示
当一个序列满足 双亲位置的值 大于或者小于 孩子位置的值的时候,就满足堆的关系,(这里为什么要叫做位置?因为一般都是在序列里面排序,储存是线性的,比大根堆和小根堆如顺序表)
#堆的种类,大根堆和小根堆
这个好理解,就是对应上面的图来说,双亲位置的值 大于 孩子位置的值 就是大根堆
双亲位置的值 小于 孩子位置的值 就是小根堆
实现堆排序,我们需要解决什么问题?
- 怎么创建一个初始的堆?即满足 双亲位置的值 大于或者小于 孩子位置的值,我们只需要关注每一个双亲的孩子是不是大于或者小于自己孩子,而不需要去管“别人家的孩子”是不是比自己家孩子大或者小
- 有了初始的堆之后,我们怎么调整剩下的元素,这个时候就需要看看“别人家的孩子”,这样处理之后,这个二叉树就满足完全二叉树的特点,按照序列排列下来就是一个有序的序列
Part1:创建初始堆,我们要考虑什么?
既然我们不需要去管整个序列是否有序,不需要去管“别人家的孩子”怎么样,那么我们先要
找到所有双亲节点。
怎么找呢?根据完全二叉树的性质:
观察每个双亲节点的序号,我们不难发现,他们的孩子节点的序号都是满足:比如双亲节点是i,那么他的左孩子就是2*i,右孩子就是2*i+1。
找到之后我们就开始调整每一个双亲位置的值和她的孩子的值:
我们这里以创建一个小根堆为例子:
我们遍历调整每个双亲节点的顺序是:
从最大的双亲节点(非终端节点)((整个顺序表的长度)/2)一直倒着来,直到下标为1的根节点
为什么是这个顺序?为什么不能倒着来?从1~length/2不是一样的么?
其实是不一样的,我们创建小根堆的目的,就是为了将最小的交换到根节点,也就是说,最后调整完初始堆,
我们的根位置的值一定是整个序列中最小的值 —— 这是很重要的性质
我们从length/2开始对每个双亲位置进行堆的调整,那么到了最后,最小的元素会出现在根位置
如果从1开始一直调整到length/2的双亲位置,那么整个序列中最小的元素,不一定会出现在根位置,因为第一次调整之后根位置的值就不再变了,只是第一个双亲位置的最小的元素。
这里有一个根据无序序列(62,25,49,25,16,8)创建小根堆的例子,顺序如下
Part2:得到了初始小根堆,我们怎么调整剩下的堆使得它有顺序
根据前面提到的选择排序的思想:
我们在这个heapsort里面怎么体现这种思想呢?
前面创建初始堆的时候,我们已经把最小的元素排出来,放在根位置了。那么我们就相当于是拿到了选择排序中的最值,这个时候我们只需要把他放在某个位置上之后,接下去就不再管它了,我们把它从序列中隔过去,在接下去的“找最值”的过程中把它忽视过去。这个“找最值”的过程就是上面Part 1 所说的,创建初始堆的过程。
我们这里算法的操作过程就是:
- 拿到最上面的根位置的值,和序列(长度n)最后一个元素交换位置。
- 然后把这个序列从后面缩小一个(序列长度n-1),也就是说,把刚刚那个元素隔过去
- 对剩下的这个被打乱的堆,再次进行Part 1的初始堆调整,我们还是想要得到剩下序列中最小的值
......(循环往复)直到 这个序列的长度变成1 这个堆排序就执行完毕,得到了一个有序的序列。
还是上面那个(62....)的序列,我们从上面得到的小根堆开始调整到有序序列的例子
#到此为止,这个堆排序就算是理解完毕了,具体怎么实现,在下面的代码中根据代码再次理解一次
1,创建顺序表,由一个int数组和一个指示长度的元素构成:
注意:这个数组是从下标为1的地方开始储存数据的!
注意:这个数组是从下标为1的地方开始储存数据的!
注意:这个数组是从下标为1的地方开始储存数据的!
#include "stdio.h"
#define Max_Num 100
typedef struct {
int record[Max_Num];
int length;
}OrderList;
2,还需要一个创建顺序表的函数
这个比较简单,也就是数组的赋值,别忘了给长度的元素赋值
OrderList CreatOrderList(int n){
int i;
OrderList orderList;
orderList.length = n;
for(i=1;i<=n;i++){
scanf("%d",&orderList.record[i]);
}
return orderList;
}
3,先简单看一下main函数的调用结构吧
首先输入长度,然后进入创建顺序表的函数之后得到一个无序的顺序表。
对这个顺序表进行核心的 堆排序操作 ,这里传送一个指针过去
然后我们把这个顺序表输出查看一下就行,printOrderList这个函数的代码会在后面给出
int main( )
{
int i,j;
int n;
printf("输入序列长度");
scanf("%d",&n);
printf("输入序列元素");
OrderList orderList = CreatOrderList(n);
HeapSort(&orderList);
printOrderList(orderList);
return 0;
}
4,最最最核心的堆排序代码部分
这个部分分成两个函数,一个是 void HeapSort(OrderList *list)这个函数控制整个堆排序算法的流程,也就是上面所说的part1,2
先创建初始堆,再递归调整剩余堆的这样两个操作。
HeapAdjust(OrderList *list, int s, int m)这个函数功能就很清楚明白,对传入的顺序表,以及传送的参数index(对应这次调整的开始位置),参数length(对应这次调整的顺序表的长度)。HeapAdjust在整个流程中有两种调用,一个是开始的创建初始堆,一个是后面的递归调整。
/**
* 这个函数有两个功能,一个是创建堆,一个是调整剩下节点
* @param list
* @param s
* @param m
*/
void HeapAdjust(OrderList *list, int index, int length) {
//保存传入节点的值
int rc;
int j;
rc = list->record[index];
for(j = 2*index;j<=length;j*=2){
//如果左子树(j=s*2)比右子树j+1的大,说明右子树更需要和双亲节点交换,则移动到record[j+1];
if((j<length)&&(list->record[j]>list->record[j+1])){
j++; //下标移动
}
//如果孩子节点的值比双亲节点的值大,说明顺序正确,不用交换,退出循环
if(rc<list->record[j]){
break;
}
//否则说明孩子节点值比双亲节点的小,交换
list->record[index] = list->record[j];
//如果换了,说明原来的双亲节点的数值被j的值覆盖,
//s的下标应该指向原来交换的地方(子节点)
index = j;
}
//原来交换的地方(子节点)应该是原来双亲节点的值,之前被rc保存,现在取出
list->record[index] = rc;
}
void HeapSort(OrderList *list){
int i;
int temp;
//循环第一次找到最后一个非叶子节点,循环下一次找到倒数第二个非叶子节点......
for(i=list->length/2 ; i>0;--i){
HeapAdjust(list,i,list->length);
}
/**
* 把堆底元素和堆顶元素进行交换之后,删除最后一个节点,对剩下的节点进行堆调整
*/
for(i=list->length;i>1;--i){
temp = list->record[1];
list->record[1] = list->record[i];
list->record[i] = temp;
HeapAdjust(list,1,i-1);
}
}
#完整代码如下:
包括顺序表的创建,输出,HeapSort和HeapAdjust
能实现的功能就是给定长度的顺序表进行堆排序并输出
#include "stdio.h"
#define Max_Num 100
typedef struct {
int record[Max_Num];
int length;
}OrderList;
void printOrderList(OrderList list){
int i;
for(i = 1;i<=list.length;i++){
printf("%d ",list.record[i]);
}
}
OrderList CreatOrderList(int n){
int i;
OrderList orderList;
orderList.length = n;
for(i=1;i<=n;i++){
scanf("%d",&orderList.record[i]);
}
return orderList;
}
/**
* 这个函数有两个功能,一个是创建堆,一个是调整剩下节点
* @param list
* @param s
* @param m
*/
void HeapAdjust(OrderList *list, int index, int length) {
//保存传入节点的值
int rc;
int j;
rc = list->record[index];
for(j = 2*index;j<=length;j*=2){
//如果左子树(j=s*2)比右子树j+1的大,说明右子树更需要和双亲节点交换,则移动到record[j+1];
if((j<length)&&(list->record[j]>list->record[j+1])){
j++; //下标移动
}
//如果孩子节点的值比双亲节点的值大,说明顺序正确,不用交换,退出循环
if(rc<list->record[j]){
break;
}
//否则说明孩子节点值比双亲节点的小,交换
list->record[index] = list->record[j];
//如果换了,说明原来的双亲节点的数值被j的值覆盖,
//s的下标应该指向原来交换的地方(子节点)
index = j;
}
//原来交换的地方(子节点)应该是原来双亲节点的值,之前被rc保存,现在取出
list->record[index] = rc;
}
void HeapSort(OrderList *list){
int i;
int temp;
//循环第一次找到最后一个非叶子节点,循环下一次找到倒数第二个非叶子节点......
for(i=list->length/2 ; i>0;--i){
HeapAdjust(list,i,list->length);
}
/**
* 把堆底元素和堆顶元素进行交换之后,删除最后一个节点,对剩下的节点进行堆调整
*/
for(i=list->length;i>1;--i){
temp = list->record[1];
list->record[1] = list->record[i];
list->record[i] = temp;
HeapAdjust(list,1,i-1);
}
}
int main( )
{
int i,j;
int n;
printf("输入序列长度");
scanf("%d",&n);
printf("输入序列元素");
OrderList orderList = CreatOrderList(n);
HeapSort(&orderList);
printOrderList(orderList);
return 0;
}
#总结:
这篇blog其实主要是是捋了捋堆排序的思路和实现过程,没有阐述堆排的优缺点和应用之类的话题,接下去的复习应该多注意一下
2018年12月9日 14点07分