对于具有 m m m 个初始归并段进行 k k k-路平衡归并时,归并的次数为: ⌊ l o g k m ⌋ ⌊log_km ⌋ ⌊logkm⌋;因此, k k k-路平衡归并中,增加 k k k / 减少初始归并段的数量 m m m 可以减少归并的次数,从而减少外存读写的次数,提高算法效率
参考:CSDN
为了防止在归并过程中某个归并段变为空,处理的办法为:可以在每个归并段最后附加一个关键字为最大值的记录。这样当某一时刻选出的冠军为最大值时,表明 5 个归并段已全部归并完成
#include
#include
#include
/*
* 首先生成大量随机数 (具体数量多少以及值的范围可以更改宏定义来修改) 存储在文件 random_num 中
*
* 之后对随机数进行外部排序 (升序),内部排序可以选择使用冒泡排序、快排、堆排,用于比较它们的性能差距;
*
* 不同归并段的归并工作则用多路平衡归并排序 (败者树) 来完成,各个归并段的结果存在文件 0, 1, 2, 3, ... 中,最后经过归并后写入文件 res,可以选择是否输出到屏幕
*
* 注:因为是在 VS 上写的,所以如果 fscanf_s fprintf_s 这些VS特有的函数编译报错,需要把它们改成不加 _s 的版本
*
*/
/***********************随机数生成**********************************/
#define MAX_VAL 10000000 // 随机数的最大值
#define MIN_VAL 0 // 随机数的最小值
#define NUM 100000000 // 用于确定随机数的个数,这里用的一亿个数,速度稍微快一点;如果十亿个数的话,就改成 10000000000
// 把这个数改大了之后需要增大下面的 BATCH_SIZE 的值来一次从外存读入更多的数据,不然外排速度太慢
#define RANDOM_FILE_NAME "random_num" // 存储数据的文件名
// 产生随机数,写入对应的文件 file_name 中,min 和 max 为生成随机数的最大、最小值,num 为生成的个数
void gen_random_num(const char file_name[], long long num, int min, int max)
{
FILE* fp = NULL;
fopen_s(&fp, file_name, "w");
int range = max - min;
while (num--)
{
int x = rand() % range + min;
fprintf(fp, "%d ", x);
}
fclose(fp);
}
/*************************排序时需要用到的数据结构、宏定义、接口函数********************************/
// 这里每次读入 BATCH_SIZE 个 int 型数据;这个值不宜过小,否则外排效率会降低,同时也不能太大,要在内存可接受的范围内
#define BATCH_SIZE 26214400 // 这里选择一次读入 100MB 的数据,栈大小设置为 120MB
typedef int Key_t; //关键字类型
typedef struct {
Key_t rec[BATCH_SIZE + 1]; // rec[0]用作哨兵
int len; // 实际的元素个数
}SqList_t;
// 接口函数,可以通过函数指针选择不同的内排方法,进行一次 BATCH_SIZE 大小的内排,排序结果写入到 sort_res 文件中
void Sort(void (*sort_func)(SqList_t* list))
{
int file_num = NUM / BATCH_SIZE + 1;
long long left_num = NUM % BATCH_SIZE;
SqList_t list;
char file_name[20] = ""; // 归并段要存入的文件名
FILE* src_fp = NULL;
fopen_s(&src_fp, RANDOM_FILE_NAME, "r");
// 将原文件分成多份,分别使用内排
for (int i = 1; i <= file_num; ++i)
{
FILE* res_fp = NULL;
int batch_size = (i == file_num) ? left_num : BATCH_SIZE;
_itoa_s(i - 1, file_name, 10, 10); // 加上文件名序号
fopen_s(&res_fp, file_name, "w");
for (int j = 1; j <= batch_size; ++j)
{
fscanf_s(src_fp, "%d", &(list.rec[j]));
}
// 对归并段进行内排
list.len = batch_size;
sort_func(&list);
for (int j = 1; j <= batch_size; ++j)
{
fprintf_s(res_fp, "%d ", list.rec[j]);
}
fclose(res_fp);
}
fclose(src_fp);
}
/*************************冒泡排序********************************/
void Bubble_sort(SqList_t* list)
{
for (int i = 0; i < list->len - 1; ++i)
{
int flag = 0;
for (int j = 1; j < list->len - i; ++j)
{
if (list->rec[j] > list->rec[j + 1])
{
list->rec[0] = list->rec[j];
list->rec[j] = list->rec[j + 1];
list->rec[j + 1] = list->rec[0];
flag = 1;
}
}
if (0 == flag)
{
break;
}
}
}
/*************************快排********************************/
//对指定序列进行一趟快排
int Partiton(SqList_t* list, int low, int high)
{
list->rec[0] = list->rec[low]; //选择枢轴
while (low < high)
{
while (high > low && list->rec[high] >= list->rec[0])
{
--high;
}
list->rec[low] = list->rec[high];
while (high > low && list->rec[low] <= list->rec[0])
{
++low;
}
list->rec[high] = list->rec[low];
}
list->rec[low] = list->rec[0];
return low; //返回枢轴位置
}
void Quick_sort_reccurent(SqList_t* list, int low, int high)
{
if (low < high)
{
int pivot_loc = Partiton(list, low, high);
Quick_sort_reccurent(list, low, pivot_loc - 1);
Quick_sort_reccurent(list, pivot_loc + 1, high);
}
}
void Quick_sort(SqList_t* list)
{
Quick_sort_reccurent(list, 1, list->len);
}
/*************************堆排********************************/
void Heap_adjust(SqList_t* list, int root, int len)
{
int min_child;
list->rec[0] = list->rec[root];
while (root * 2 <= len)
{
min_child = root * 2;
if (min_child + 1 <= len
&& list->rec[min_child + 1] > list->rec[min_child])
{
++min_child;
}
if (list->rec[min_child] > list->rec[0])
{
list->rec[root] = list->rec[min_child];
root = min_child;
}
else {
break;
}
}
list->rec[root] = list->rec[0];
}
void Heap_sort(SqList_t* list)
{
//初建堆
for (int i = list->len / 2; i >= 1; --i)
{
Heap_adjust(list, i, list->len);
}
//输出 n-1 次,调整堆 n-2 次
for (int i = 0; i < list->len - 1; ++i)
{
if (i > 0)
{
Heap_adjust(list, 1, list->len - i);
}
list->rec[0] = list->rec[1];
list->rec[1] = list->rec[list->len - i];
list->rec[list->len - i] = list->rec[0];
}
}
/*************************归并 (败者树)********************************/
#define K ((NUM % BATCH_SIZE == 0) ? NUM / BATCH_SIZE : NUM / BATCH_SIZE + 1) // K 路平衡归并排序 K 为归并段的个数
typedef struct LoserTree{
Key_t leaf[K]; // 败者树的叶结点,每个归并段都将数据送到对应的叶结点进行归并
int tree[K]; // 非叶节点,用来指示败者序号,tree[0]为最后的胜者序号
}LoserTree_t;
// 进行一次败者树重构
int LoserTree_adjust(LoserTree_t* loser_tree, int winner_idx)
{
int father_idx = (winner_idx + K) / 2; // 父结点的序号
while (father_idx != 0)
{
if (loser_tree->leaf[winner_idx] > loser_tree->leaf[loser_tree->tree[father_idx]])
{
// winner_idx 为败者
int tmp = winner_idx;
winner_idx = loser_tree->tree[father_idx]; // 重新记录胜者名
loser_tree->tree[father_idx] = tmp; // 把败者的名字写到败者树上去
}
father_idx /= 2; // 向上层移动
}
loser_tree->tree[0] = winner_idx; // 记录最后的胜者
// 最后选出的胜者为最大的可能值 + 1,则表明所有归并段都已读完,外排结束
if (loser_tree->leaf[loser_tree->tree[0]] == MAX_VAL)
{
return 0;
}
return 1;
}
// 构建败者树
void LoserTree_build(LoserTree_t* loser_tree)
{
int winner_idx[K];
for (int i = K - 1; i != 0; --i) // 从第一个非叶节点开始更新
{
Key_t left_child = (2 * i > K - 1) ? loser_tree->leaf[2 * i - K] : loser_tree->leaf[winner_idx[2 * i]];
Key_t right_child = (2 * i + 1 > K - 1) ? loser_tree->leaf[2 * i + 1 - K] : loser_tree->leaf[winner_idx[2 * i + 1]];
// 记录败者名
if (left_child > right_child)
{
loser_tree->tree[i] = (2 * i > K - 1) ? (2 * i - K) : winner_idx[2 * i];
winner_idx[i] = (2 * i + 1 > K - 1) ? (2 * i + 1 - K) : winner_idx[2 * i + 1];
}
else {
loser_tree->tree[i] = (2 * i + 1 > K - 1) ? (2 * i + 1 - K) : winner_idx[2 * i + 1];
winner_idx[i] = (2 * i > K - 1) ? (2 * i - K) : winner_idx[2 * i];
}
}
loser_tree->tree[0] = winner_idx[1];
}
// 败者树归并
void LoserTree_merge(LoserTree_t* loser_tree, int print_flag)
{
// 先从所有归并段中读出一个数据存入败者树的叶结点中
FILE* files[K];
char file_name[20];
FILE* res_flie = NULL;
Key_t last_num = MIN_VAL;
fopen_s(&res_flie, "res", "w");
for (int i = 0; i < K; ++i)
{
// 初始化文件指针
_itoa_s(i, file_name, 10, 10); // 加上文件名序号
fopen_s(&files[i], file_name, "r");
fscanf_s(files[i], "%d", &(loser_tree->leaf[i]));
}
LoserTree_build(loser_tree);
int output_file_idx; // 包含最小值的归并段的序号
do {
output_file_idx = loser_tree->tree[0]; // 包含最小值的归并段的序号
fprintf_s(res_flie, "%d ", loser_tree->leaf[output_file_idx]); // 输出最小值
if (print_flag)
{
printf("%d ", loser_tree->leaf[output_file_idx]);
}
if (last_num > loser_tree->leaf[output_file_idx])
{
// 如果不是升序,则报错
printf("\nERR!!!\n");
return;
}
last_num = loser_tree->leaf[output_file_idx];
if (!feof(files[output_file_idx]))
{
fscanf_s(files[output_file_idx], "%d", &(loser_tree->leaf[output_file_idx]));
}
else {
loser_tree->leaf[output_file_idx] = MAX_VAL; // 如果一个归并段读完了,就在最后插入一个最大值+1的数,表示该归并段结束
}
} while (LoserTree_adjust(loser_tree, output_file_idx));
for (int i = 0; i < K; ++i)
{
fclose(files[i]);
}
}
int main(int argc, const char* argv[])
{
/*************************生成随机数*************************/
// 不需要生成随机数时可以注释掉
printf("Generating random num...\n");
gen_random_num("random_num", NUM, MIN_VAL, MAX_VAL);
printf("random num generated! saved to file \"random_num\"\n");
/****************将大量数据分成多个归并段进行内排,下面三种内排方法可以随便选择一个********************/
printf("正在内排...\n");
// Sort(Quick_sort);
// Sort(Bubble_sort);
Sort(Heap_sort);
printf("内排完成! 归并段结果保存在文件 \"0\", \"1\", \"2\"... 中\n");
/****************用败者树对多个归并段进行归并*********************/
printf("正在归并所有归并段...\n");
LoserTree_t loser_tree;
LoserTree_merge(&loser_tree, 0); // 1 表示同时输出到屏幕,0 表示只输出到文件
printf("归并完成! 最终结果保存在文件 \"res\" 中\n");
return 0;
}
MINIMAX
记录MINIMAX
记录输出到归并段文件中MINIMAX
值大的记录中选出值最小的关键字的记录,作为新的 MINIMAX
记录;MINIMAX
记录为止,由此就得到了一个初始归并段;MINIMAX
记录,同时将其输出到归并段文件中,如下图所示:MINIMAX
值输出到 归并段文件中,如下图所示:MINIMAX
值输出到归并段文件中,如下图所示:MINIMAX
值为止,表示一个归并段已经生成,则开始下一个归并段的创建,如下图所示:MINIMAX
记录