很多排序算法,插入排序、选择排序、归并排序等,这些算法都属于内部排序算法,排序的整个过程只是在内存中完成。而当待排序的文件比内存的可使用容量还大时,文件无法一次性放到内存中进行排序,需要借助于外部存储器(例如硬盘、U盘、光盘),这时要用到外部排序算法来解决。
外部排序算法由两个阶段构成:
1、按照内存大小,将大文件分成若干长度为l的子文件(l应小于内存的可使用容量),然后将各个子文件依次读入内存,使用适当的内部排序算法对其进行排序(排好序的子文件称为归并段或者顺段),将排好序的归并段重新写入外存,为下一个子文件排序腾出内存空间。
2、对得到的归并段进行合并,直至得到整个有序的文件为止。
/**
* 实验题目:
* 用败者树实现置换-选择排序算法
* 实验目的:
* 领会外排序中置换-选择排序算法的执行过程和算法设计
* 实验内容:
* 编写程序,模拟置换-选择算法生成初始归并段的过程以求解以下问题:
* 设磁盘文件中共有18个记录,记录的关键字序列为:(15, 4, 97, 64, 17, 32, 108,
* 44, 76, 9, 39, 82, 56, 31, 80, 73, 255, 68)
* 若内存工作区可容纳5个记录,用置换-选择排序可产生几个初始归并段,每个
* 初始归并段包含哪些记录?假设输入文件数据和输出归并段数据均放在内存中。
*/
#include <stdio.h>
#define MAX_SIZE 50 // 每个文件最多记录数
#define MAX_KEY 32767 // 最大关键字值∞
#define W 5 // 内存工作区可容纳的记录个数
typedef int key_type; // 定义关键字类型为整型
typedef int info_type; // 定义其他数据项的类型
typedef int loser_tree; // 败者树结点类型,它是完全二叉树且不含叶子,采用顺序存储结构
typedef struct
{
key_type key; // 关键字项
info_type other_info; // 其他数据项,具体类型在主程序中定义,这里假设为int
}rec_type; // 排序文件的记录类型
typedef struct
{
rec_type recs[MAX_SIZE]; // 存放文件中的数据项
int length; // 存放文件中实际记录个数
int cur_rec; // 存放当前位置
}file_type; // 文件类型
typedef struct
{
rec_type rec; // 存放记录
int r_num; // 所属归并段的段号
}work_area; // 内存工作区元素类型,其容量为W
file_type fi; // 定义输入文件,为全局变量
file_type fo; // 定义输出文件,为全局变量
/*-------------------输入输出文件初始化---------------------*/
static void initial(void)
{
int i;
int n = 19;
key_type a[] = {15, 4, 97, 64, 17, 32, 108, 44, 76, 9, 39, 82, 56, 31, 80, 73, 255, 68, MAX_KEY};
for(i = 0; i < n; i++) // 将n个记录存放到输入文件中
fi.recs[i].key = a[i];
fi.length = n; // 输入文件有n个记录
fi.cur_rec = -1; // 输入文件中当前位置为-1
fo.cur_rec = -1; // 输出文件中当前位置为-1
fo.length = 0; // 输出文件中没有任何记录
}
/*-----------------从wa[q]起到败者树的根比较选择最小关键字的记录,并由q指示它所在的归并段-----------------*/
static void select_minimax(loser_tree ls[W], work_area wa[W], int q)
{
int p, s, t;
for(t = (W + q) / 2, p = ls[t]; t > 0; t = t / 2, p = ls[t])
{
if(wa[p].r_num < wa[q].r_num || wa[p].r_num == wa[q].r_num && wa[p].rec.key < wa[q].rec.key)
{
s = q;
q = ls[t]; // q指向新的胜者
ls[t] = s;
}
}
ls[0] = q; // 根节点
}
/*-----------------输入W个记录到内存工作区wa,创建败者树ls,选最小的记录并由s指示其在wa中的位置-------------*/
static void construct_loser(loser_tree ls[W], work_area wa[W])
{
int i;
for(i = 0; i < W; i++)
wa[i].r_num = wa[i].rec.key = ls[i] = 0; // 工作区初始化
for(i = W - 1; i >= 0; i--)
{
fi.cur_rec++; // 从输入文件读入一个记录
wa[i].rec = fi.recs[fi.cur_rec];
wa[i].r_num = 1; // 其段号为1
select_minimax(ls, wa, i); // 调整败者
}
}
/*-----------------求一个初始归并段--------------------*/
static void get_run(loser_tree ls[W], work_area wa[W], int rc, int &rmax) // 引用
{
int q;
key_type mini_max; // 当前最小关键字
while(wa[ls[0]].r_num == rc) // 选得的当前最小记录属于当前段时
{
q = ls[0]; // q指向当前最小记录在wa中的位置
mini_max = wa[q].rec.key;
fo.cur_rec++; // 将刚选得的当前最小记录写入输出文件
fo.length++;
fo.recs[fo.cur_rec] = wa[q].rec;
fi.cur_rec++; // 从输入文件读入下一记录
wa[q].rec = fi.recs[fi.cur_rec];
if(fi.cur_rec >= fi.length - 1) // 输入文件结束,虚设记录(属rmax+1段)
{
wa[q].r_num = rmax + 1;
wa[q].rec.key = MAX_KEY;
}
else // 输入文件非空时
{
if(wa[q].rec.key < mini_max) // 新读入的记录属于下一段
{
rmax = rc + 1;
wa[q].r_num = rmax;
}
else // 新读入的记录属于当前段
wa[q].r_num = rc;
}
select_minimax(ls, wa, q); // 选择新的当前最小记录
}
}
/*-----------------在败者树ls和内存工作区wa上用置换-选择排序求初始归并段--------------*/
static void replace_selection(loser_tree ls[W], work_area wa[W])
{
int rc;
int rmax;
rec_type j; // j作为一个关键字最大记录,作为一个输出段结束标志
j.key = MAX_KEY;
construct_loser(ls, wa); // 初建败者树
rc = 1; // rc指示当前生成的初始归并段的段号
rmax = 1; // rmax指示wa中关键字所属初始归并段的最大段号
while(rc <= rmax) // rc = rmax+1标志输入文件的置换-选择排序已完成
{
get_run(ls, wa, rc, rmax); // 求得一个初始归并段
fo.cur_rec++; // 将段结束标志写入输出文件
fo.recs[fo.cur_rec] = j;
fo.length++;
rc = wa[ls[0]].r_num; // 设置下一段的段号
}
}
int main(void)
{
int i = 0; // 循环变量
int rno = 1; // 归并段个数
initial();
loser_tree ls[W]; // 败者树数组
work_area wa[W]; // 工作区数组
printf("大文件的记录为:\n ");
while(fi.recs[i].key != MAX_KEY)
{
printf("%d ", fi.recs[i].key);
i++;
}
printf("\n");
replace_selection(ls, wa); // 用置换-选择排序求初始归并段
printf("产生的归并段文件的记录如下:\n");
printf(" 归并段%d:", rno); // 输出所有的归并段
for(i = 0; i < fo.length; i++)
{
if(fo.recs[i].key == MAX_KEY)
{
printf("∞");
if(i < fo.length - 1)
{
rno++;
printf("\n 归并段%d:", rno);
}
}
else
printf("%d ", fo.recs[i].key);
}
printf("\n 共产生%d个归并段文件\n", rno);
return 0;
}
测试结果:
大文件的记录为:
15 4 97 64 17 32 108 44 76 9 39 82 56 31 80 73 255 68
产生的归并段文件的记录如下:
归并段1:4 15 17 32 44 64 76 82 97 108 ∞
归并段2:9 31 39 56 68 73 80 255 ∞
共产生2个归并段文件