大根堆:
根结点(称为堆顶)的关键字是堆里所有结点关键字中最大者,称为大根堆,又称最大堆(大顶堆)。大根堆要求根节点的关键字既大于或等于左子树的关键字值,又大于或等于右子树的关键字值。
Heap是一种数据结构具有以下的特点:
1) 完全二叉树;
2)heap中存储的值是偏序;
小根堆(最大堆): 父节点的值小于或等于子节点的值;
大根堆(最小堆): 父节点的值大于或等于子节点的值;
堆的存储
一般都用数组来表示堆,i结点的父结点下标就为(i–1)/2。它的左右子结点下标分别为2 * i + 1和2 * i + 2。如第0个结点左右子结点下标分别为1和2。
堆排序
堆建好之后堆中第0个数据是堆中最大的数据。取出这个数据再执行下堆的删除操作。这样堆中第0个数据又是堆中最大的数据,重复上述步骤直至堆中只有一个数据时就直接取出这个数据。
堆的操作:insert
插入一个元素:新元素被加入到heap的末尾,然后更新树以恢复堆的次序。
每次插入都是将新数据放在数组最后。可以发现从这个新数据的父结点到根结点必然为一个有序的数列,现在的任务是将这个新数据插入到这个有序数据中——这就类似于直接插入排序中将一个数据并入到有序区间中。
堆的操作:Removemax
按定义,堆中每次都删除第0个数据。为了便于重建堆,实际的操作是将最后一个数据的值赋给根结点,然后再从根结点开始进行一次从上向下的调整。调整时先在左右儿子结点中找最大的,如果父结点比这个最小的子结点还大说明不需要调整了,反之将父结点和它交换后再考虑后面的结点。相当于从根结点将一个数据的“下沉”过程。
/**
* 实验题目:
* 从大数据文件中挑选K个最小的记录
* 实验目的:
* 掌握外排序的过程及堆的应用算法设计
* 实验内容:
* 编写程序,从大数据文件中挑选K个最小的记录。假设内存工作区的大小为K,
* 模拟这个过程,并输出每趟的结果。假设整数序列为(15, 4, 97, 64, 17, 32, 108,
* 44, 76, 9, 39, 82, 56, 31, 80, 73, 255, 68),从中挑选5个最小的整数。
* 备注:
* 为了简单,假设每个记录仅仅包含整型关键字,用全局变量fi模拟存放所有的记录,
* R[1..K]存放大根堆。
*/
#include <stdio.h>
#include <stdbool.h>
#define MAX_SIZE (100)
#define MAX_KEY (32767) // 最大关键字值∞
#define K 5 // 内存工作区可容纳的记录个数
typedef int key_type; // 关键字类型
typedef struct
{
key_type recs[MAX_SIZE]; // 存放文件中的数据项
int length; // 存放文件中实际记录个数
int cur_rec; // 存放当前位置
}file_type; // 文件类型
file_type fi; // 定义输入文件,为全局变量
key_type R[K + 1]; // 存放大根堆
/*------------------输入文件初始化-------------------*/
static void initial(void)
{
int i;
int n = 18;
key_type a[] = {15, 4, 97, 64, 17, 32, 108, 44, 76, 9, 39, 82, 56, 31, 80, 73, 255, 68};
for(i = 0; i < n; i++) // 将n个记录存放到输入文件中
fi.recs[i] = a[i];
fi.length = n; // 输入文件中有n个记录
fi.cur_rec = -1; // 输入文件中当前位置为-1
}
/*-------------------从输入文件中取一个记录r--------------------*/
static bool get_one_rec(key_type &r)
{
fi.cur_rec++;
if(fi.cur_rec == fi.length)
return false;
else
{
r = fi.recs[fi.cur_rec];
return true;
}
}
/*-------------------显示堆中所有记录--------------------*/
static void disp_heap(void)
{
int i;
for(i = 1; i <= K; i++)
printf("%d ", R[i]);
printf("\n");
}
/*---------------筛选为大根堆算法------------------*/
static void sift(int low, int high)
{
int i = low;
int j = 2 * i; // R[j]是R[i]的左孩子
key_type tmp = R[i];
while(j <= high)
{
if(j < high && R[j] < R[j + 1]) // 若右孩子较大,把j指向右孩子
j++; // 变为2i+1
if(tmp < R[j])
{
R[i] = R[j]; // 将R[j]调整到双亲结点位置上
i = j; // 修改i和j的值,以便继续向下筛选
j = 2 * i;
}
else
break; // 筛选结束
}
R[i] = tmp; // 被筛选结点的值放入最终位置
}
/**
* 功能:
* 从输入文件fi中挑选K个最小的记录。
* 算法思路:
* 首先,从fi中取出开头的K个记录存放在R中,将其调整为一个大根堆R,
* 然后依次取出fi的其余记录r,若r小于大根堆R的根结点R[1],用r替代R[1],
* 再筛选为大根堆。当fi所有记录取出完毕,R中即为K个最小的记录。
*/
static void select_k(void)
{
int i;
key_type r;
for(i = 0; i < K; i++) // 从输入文件fi中取出开头的K个记录存放在R[1...K]中
{
get_one_rec(r);
R[i + 1] = r; // R[1...5]依次为15, 4, 97, 64, 17
}
for(i = K / 2; i >= 1; i--) // 建立初始堆 i = 2, 1
sift(i, K);
printf("开头%d个记录创建的大根堆:", K);
disp_heap();
while(get_one_rec(r)) // 从输入文件fi中取出其余的记录
{
printf(" 处理%d:", r);
if(r < R[1]) // 若r小于堆的根结点
{
R[1] = r; // 用r替代堆的根结点
sift(1, K); // 继续筛选
printf("\t需要筛选,结果:");
disp_heap();
}
else
printf("\t不需要筛选\n");
}
}
int main(void)
{
initial();
select_k();
printf("最终结果: ");
disp_heap();
return 0;
}
测试结果:
开头5个记录创建的大根堆:97 64 15 4 17
处理32: 需要筛选,结果:64 32 15 4 17
处理108: 不需要筛选
处理44: 需要筛选,结果:44 32 15 4 17
处理76: 不需要筛选
处理9: 需要筛选,结果:32 17 15 4 9
处理39: 不需要筛选
处理82: 不需要筛选
处理56: 不需要筛选
处理31: 需要筛选,结果:31 17 15 4 9
处理80: 不需要筛选
处理73: 不需要筛选
处理255: 不需要筛选
处理68: 不需要筛选
最终结果: 31 17 15 4 9