外排序中的归并排序
- 7.11 外排序中的归并排序
- 相关基础知识
- 原理
- 最终参考程序
7.11 外排序中的归并排序
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。(下文也称外排序)
例如 1 G = = 102 4 3 b y t e 1G==1024^3 byte 1G==10243byte,一个int型整数4byte,100亿( 1 0 10 10^{10} 1010)个int型整数占用空间为37G左右,针对这100亿个数据的排序已无法用常见的排序算法去操作。因为内存无法容纳如此庞大的数据,这些数据必定是保存在磁盘中。
归并排序是将数据分割成若干个细小的单位,再将这些细小的单位合并成一个整体,合并的时候按照某种排序规则进行。所以归并排序不仅可以做内排序,还能充当外排序的重要角色。
相关基础知识
外排序是针对文件操作的,所以需要掌握和文件相关的函数和命令。
测试环境是windows 11,所以命令以windows 11适用为主。
可能用到的:
int scanf(const char* format[,argument]...);
scanf能将从键盘上读取的数据存放进变量。
int fscanf(FILE* stream , const char* format[, argument ]...);
fscanf功能和scanf类似,但不同的是fscanf可以指定操作文件也就是形参stream。当stream为stdin(标准输入流)时和scanf几乎无区别。
int sscanf(const char* buffer,const char* format[,argument] ...);
sscanf将数据读取对象从文件变成了指定字符串。
int printf(const char* format [,argument*]*... );
printf能将数据以字符串的形式打印在终端控制台(windows叫cmd)。
int fprintf(FILE* stream,const char* format[,argument*]*...);
fprintf能将数据以字符串的形式打印在指定文件。当stream为stdout(标准输出流)时和printf几乎无区别。
int sprintf(const char* buffer,const char* format[,argument*]*...);
sprintf将数据输出的对象从文件变成了字符串。
FILE* fopen(const char* filename,const char* mode);
返回文件filename的操作模式为mode的文件指针。
int system(const char* command);
system能调用命令行让cmd执行各种操作。
int snprintf(char* s,size_t n,const char *format,...);
和sprintf相比,它能指定输出n个字符到字符串s,并返回成功输出的字符数。于是我们可以这样操作:int name_len = snprintf(NULL,0,"midFile_%d_%d.txt",level,fnum);
,这个函数可以返回文件名的长度,因为是指定输出0个字符到NULL地址处的“字符串”,所以函数内部并没有产生实质性的指针调用操作。
-
windows OS,cmd的命令:
move src dis
将src路径的某文件改名并移动到dis路径。del file
删除file文件。md route
创建路径(目录)。rd route
删除路径(目录)。
原理
- 将数据存放在一个可读的文本文件中(其他拓展名的应该也行,关键是程序和自己都能识别)。
- 读取这个文件的数据,读完一段数据之后,用内排序处理,将处理完的数据输出为中间文件。
中间文件的命名不唯一,我选择的格式是
midFile_level_num.txt
,level是级别,num是文件的编号,这样以来,每个level
级的中间文件都是由level-1
级的文件合并而来。中间文件最好放在一个文件夹中统一保存。
- 将所有的中间文件两两归并,生成更高一级的中间文件,并删除参与归并的低一级的文件(磁盘容量充裕的话可忽略)。
偶数个中间文件归能全部归并完成并删除,奇数个中间文件会留一个,那个文件改名成高一级的中间文件即可。
- 重复3的操作,直到只剩最后一个文件,那个文件即为排序好的数据。
原理其实和归并排序差不多,但有磁盘作为存储数据的介质,可操作空间非常大。
最终参考程序
fileSort.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
typedef int Datatype;
//创造数据
void createData(int num);
//判断数据是否有序
int judgSort(Datatype* a, int n);
//用归并算法将文件中的数据进行排序
void mergeSortFile(const char* st);
fileSort.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"fileSort.h"
//交换函数
void Swap(Datatype* a, Datatype* b) {
int t = *a;
*a = *b;
*b = t;
}
//创造数据
void createData(int num) {
srand((size_t)(0));
char st[] = "beforeSortData.txt";
FILE* fout = fopen(st, "w");
int i = 0;
for (i = 0; i < num; i++)
fprintf(fout, "%d\n", rand() * rand() + 1);
fclose(fout);
}
//判断数据是否有序
int judgSort(Datatype* a, int n) {
int i = 0;
for (i = 0; i < n - 1; i++)
if (a[i] > a[i + 1])
break;
return i == n - 1;
}
//插入排序
void insertSort(Datatype* a, int n) {
int i = 0;
for (i = 0; i < n - 1; ++i) {
int end = i;
int tmp = a[i + 1];
while (end >= 0) {
if (a[end] > tmp) {
a[end + 1] = a[end];
--end;
}
else
break;
}
a[end + 1] = tmp;
}
}
//生成中间文件
void buyMidFile(Datatype* a, int n, int level, int* fnum) {
int nlen = 64;//中间文件名字最大长度
char* midFileName = (char*)malloc(sizeof(char) * nlen);
if (midFileName == NULL) {
printf("buyMidFile malloc 1");
perror("");
exit(-1);
}
//生成1个文件数,计数变量增加
(*fnum)++;
//文件名长度
int flen = snprintf(NULL, 0,
".\\sub\\midFile_%d_%d.txt", level, *fnum);
while (flen > nlen) {
char* tmp = (char*)realloc(midFileName, nlen * 2 * sizeof(char));
if (tmp == NULL) {
printf("buyMidFile realloc 2");
perror("");
exit(-1);
}
midFileName = tmp;
nlen *= 2;
}
flen = sprintf(midFileName,
".\\sub\\midFile_%d_%d.txt", level, *fnum);
FILE* fout = fopen(midFileName, "w");
if (fout == NULL) {
system("md sub");//可能是文件夹不存在的情况
fout = fopen(midFileName, "w");
if (fout == NULL) {
printf("%s打开失败", midFileName);
exit(-1);
}
}
int i = 0;
for (i = 0; i < n; i++)
fprintf(fout, "%d\n", a[i]);
free(midFileName);
fclose(fout);
}
//切割文件
int inciseFile(const char* st) {
//打开源文件
FILE* srcData = fopen(st, "r");
if (srcData == NULL) {
printf("%s打开失败", st);
exit(-1);
}
//申请数组
Datatype* a = (Datatype*)malloc(sizeof(Datatype) * 10);
if (a == NULL) {
printf("inciseFile malloc 1");
perror("");
exit(-1);
}
int ai = 0;//和a配套的指针变量
int srcp = 0;//文件阅读进度标记
int tnum = 0;//临时变量
int fnum = 0;//生成的文件数
while (srcp = fscanf(srcData, "%d", &tnum), srcp != EOF) {
a[ai++] = tnum;
if (ai == 10) {//读满10就排序
insertSort(a, ai);
buyMidFile(a, ai, 1, &fnum);
ai = 0;
}
}
if (ai != 0) {
insertSort(a, ai);
buyMidFile(a, ai, 1, &fnum);
ai = 0;
}
return fnum;
}
//归并文件
int mergeFile(int level, int last_num) {
int this_num = 0;//记录这一层生成的文件数
FILE* file1 = NULL, * file2 = NULL;//参与归并的两个文件
int fnlen = 64;//文件名的最大长度
char* file_name = (char*)malloc(sizeof(char) * fnlen);
if (file_name == NULL) {
printf("mergeFile malloc 1");
perror("");
exit(-1);
}
int i = 1;
int n1 = 0, n2 = 0, p1 = 0, p2 = 0;//临时变量n,文件阅读追踪变量p
for (i = 1; i < last_num; i += 2) {//枚举上一层的文件
int name_len =
snprintf(NULL, 0,
"sub\\midFile_%d_%d.txt", level - 1, i);
//扩容兼检查容量
while (fnlen < name_len) {
char* tmp = (char*)realloc
(file_name, fnlen * 2 * sizeof(char));
if (tmp == NULL) {
printf("mergeFile realloc 2");
perror("");
exit(-1);
}
file_name = tmp;
fnlen *= 2;
}
//打开三个要操作的文件
sprintf(file_name, "sub\\midFile_%d_%d.txt", level - 1, i);
file1 = fopen(file_name, "r");
if (file1 == NULL) {
printf("%s打开失败", file_name);
exit(-1);
}
sprintf(file_name, "sub\\midFile_%d_%d.txt", level - 1, i + 1);
file2 = fopen(file_name, "r");
if (file2 == NULL) {
printf("%s打开失败", file_name);
exit(-1);
}
sprintf(file_name, "sub\\midFile_%d_%d.txt", level, ++this_num);
FILE* final_file = fopen(file_name, "w");
if (final_file == NULL) {
printf("%s打开失败", file_name);
exit(-1);
}
//文件归并
p1 = fscanf(file1, "%d", &n1);
p2 = fscanf(file2, "%d", &n2);
while (p1 != EOF && p2 != EOF) {
if (n1 < n2) {
fprintf(final_file, "%d\n", n1);
p1 = fscanf(file1, "%d", &n1);
//读完最后一个数后p1即为EOF,
//若不加,则少读1个数据
}
else {
fprintf(final_file, "%d\n", n2);
p2 = fscanf(file2, "%d", &n2);
}
}
while (p1 != EOF) {
fprintf(final_file, "%d\n", n1);
p1 = fscanf(file1, "%d", &n1);
}
while (p2 != EOF) {
fprintf(final_file, "%d\n", n2);
p2 = fscanf(file2, "%d", &n2);
}
fclose(file1);
fclose(file2);
file1 = file2 = NULL;
fclose(final_file);
//删除上一级的中间文件
sprintf(file_name, "del .\\sub\\midFile_%d_%d.txt", level - 1, i);
system(file_name);
sprintf(file_name, "del .\\sub\\midFile_%d_%d.txt", level - 1, i + 1);
system(file_name);
}
//处理奇数个中间文件的情况
if (i == last_num) {
int name_len = snprintf(NULL, 0,
"move .\\sub\\midFile_%d_%d.txt .\\sub\\midFile_%d_%d.txt",
level - 1, i, level, ++this_num);
while (fnlen < name_len) {
char* tmp = (char*)realloc
(file_name, fnlen * 2 * sizeof(char));
if (tmp == NULL) {
printf("mergeFile realloc 2");
perror("");
exit(-1);
}
file_name = tmp;
fnlen *= 2;
}
sprintf(file_name,
"move .\\sub\\midFile_%d_%d.txt .\\sub\\midFile_%d_%d.txt",
level - 1, i, level, this_num);
system(file_name);
}
free(file_name);
return this_num;
}
//用归并算法将文件中的数据进行排序
void mergeSortFile(const char* st) {
//1、将文件分割10个为1组的多个小文件。
int last_num = inciseFile(st);//last num
//2、两个文件归并成一个,变归并边删除子文件。
int level = 2;//这一层的文件数,等级
while (last_num > 1) {
last_num = mergeFile(level, last_num);//归并生成的等级,上层文件数
++level;
}
//3、归并到最后一个,再将文件改名
int len = snprintf(NULL, 0,
"move .\\sub\\midFile_%d_1.txt .\\afterSortData.txt", level - 1);
char* tmp = (char*)malloc(sizeof(char) * len);
if (tmp == NULL) {
printf("mergeSortFile malloc 1");
perror("");
exit(-1);
}
sprintf(tmp,
"move .\\sub\\midFile_%d_1.txt .\\afterSortData.txt",
level - 1);
system(tmp);
system("rd sub");
printf("beforeSortData.txt排序成功\n");
}
test.c
#ifndef _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS 1
#endif
#include"FileSort.h"
void f0() {//生成随机数文件
createData(114514);
}
void f1() {//文件归并
mergeSortFile("beforeSortData.txt");//归并排序
}
int main()
{
f0();//生成随机数文件
f1();//文件归并
return 0;
}