Welcome to 9ilk's Code World
(๑•́ ₃ •̀๑) 个人主页: 9ilk
(๑•́ ₃ •̀๑) 文章专栏: 数据结构
我们之前学习的八大排序:冒泡,快排,插入,堆排等都是内排序,这些排序算法处理的都是在内存中的数据,如果我们要处理的数据量过大,不能一次性装进内存中呢?此时我们需要了解我们的新知识 --- 外排序
🏠 什么是外排序
📒 外排序介绍
外排序(External sorting)是指能够处理极⼤量数据的排序算法。
外排序通常采⽤的是⼀种“排序-归并”的策略。在排序阶段,先读入能放在内存中的数据量,将其排序输出到⼀个临时文件,依此进行,将待排序数据组织为多个有序的临时文件。然后在归并阶段将这些临时文件组合为⼀个大的有序文件,也即排序结果。
注意:
1. 通常来说,外排序处理的数据不能一次性装入内存,只能放在读写较慢的外存储器(通常是硬盘上)。
2. 我们之前常见的排序是内排,它们排序思想适应的是数据在内存中支持随机访问。归并排序的思想不需要随机访问数据,只需要依次序列读取数据,所以归并排序既是一个内排序,也是一个外排序。
📒 文件归并排序思路分析
也许有朋友看到文件归并排序会想到,将一个大文件先细分成n个小文件,再各自各自地进行归并,如下图:
这种做法是可行的是但是比较麻烦的是一开始划分小文件的个数以及每个小文件的大小,实现较为复杂,因此我们选择如下的归并思路:
- 读取n个值排序后写到file1,再读取n个值排序后写到file2
- file1和file2利⽤归并排序的思想,依次读取⽐较,取⼩的尾插到mfile,mfile归并为⼀个有序⽂件
- 将file1和file2删掉,mfile重命名为file1
- 再次读取n个数据排序后写到file2
-
继续⾛file1和file2归并,重复步骤2,直到⽂件中⽆法读出数据。最后归并出的有序数据放到了file1中
🏠 文件归并排序代码实现
📌 生成大文件
这里我们需要复习C语言库里面的几个关于文件的接口:
fopen : 打开文件,返回一个FILE*的指针。(const char * filename, const char * mode)
fclose : 关闭文件。( FILE * stream)
fprintf : 将格式化数据写进文件。(FILE * stream, const char * format, ...)
fscanf : 从文件中读取格式化数据。(FILE * stream, const char * format, ...)
具体用法可自行查文档哦 ~
参考代码:
void CreateData()
{
FILE* fin = fopen("data.txt", "w");
if (fin == NULL)
{
perror("fopen fail");
return;
}
const int N = 1000000;
srand(time(0));
//读入N个数 随机数
for (int i = 0; i < N; i++)
{
int num = rand() + i;
fprintf(fin, "%d\n", num);
}
fclose(fin);
}
📌 小文件输入数据并排序
我们可以利用一个容器利用fscanf从bigfile里读取数据,再用fprintf将读取的数据排序后输入到目标小文件内
//分别读入数据进file1和file2并且排序
void RDatatoSortFile(const char* file1,FILE* bigfile,int n)
{
vector<int> v;
v.resize(n);
FILE* file = fopen(file1, "w");
if (file == NULL)
{
perror("fopen fail");
return;
}
//从bigfile读取n个数据进数组
for (int i = 0; i < n; i++)
{
int num = 0;
fscanf(bigfile, "%d", &num);
v.push_back(num);
}
//排序
sort(v.begin(), v.end());
//将数据读入file
for (int i = 0; i < n; i++)
{
fprintf(file, "%d\n", v[i]);
}
fclose(file);
fclose(bigfile);
}
注意:
- 当bigFile里数据要读取完时,读取数据个数可能不足n个数据,这时我们可以利用fscanf的返回值(读到文件末尾->EOF)和一个变量j来判断是否读完了。
- 同时我们可以修改返回类型为int,当返回0说明文件都读完了。
修改代码:
//分别读入数据进file1和file2并且排序
int RDatatoSortFile(const char* file1,FILE* bigfile,int n)
{
vector<int> v;
v.resize(n);
FILE* file = fopen(file1, "w");
if (file == NULL)
{
perror("fopen fail");
return;
}
//从bigfile读取n个数据进数组
int j = 0;
for (int i = 0; i < n; i++)
{
int num = 0;
num = fscanf(bigfile, "%d", &num);
if (num == EOF)
break;
v.push_back(num);
j++;
}
//数据读完提前返回
if (j == 0)
return 0;
sort(v.begin(), v.end());
//将数据读入file
for (int i = 0; i < j; i++)
{
fprintf(file, "%d\n", v[i]);
}
fclose(file);
return j;
}
注意:输入完数据后不能先fclose(bigfile),这样会导致bigfile文件指针读到了文件末尾,后续输入不进数据因为一直在文件末尾。
📌 合并file1和file2为mfile
读取file1和file2内的数据,小的先进mfile继续读取这个文件;最后一定有个文件先读完,将剩下的文件继续读完。
参考代码:
void mergeFile(const char* file1,const char* file2,const char* mfile)
{
FILE* File1 = fopen(file1, "r");
if (file1 == NULL)
{
perror("fopen fail");
return;
}
FILE* File2 = fopen(file2, "r");
if (file2 == NULL)
{
perror("fopen fail");
return;
}
FILE* Mfile = fopen(mfile, "w");
if (mfile == NULL)
{
perror("fopen fail");
return;
}
//fscanf读取数据成功都返回的是1 错误返回的是-1 所以要另外开两个变量接收
int x1 = 0;
int ret1 = 0;
ret1 = fscanf(File1, "%d", &x1);
int ret2 = 0;
int x2 = 0;
ret2 = fscanf(File2, "%d", &x2);
while (ret1 != EOF && ret2 != EOF)
{
if (x1 <x2)
{
//读进mfile
fprintf(Mfile, "%d\n", x1);
ret1 = fscanf(File1, "%d", &x1);
}
else
{
fprintf(Mfile, "%d\n", x2);
ret2 = fscanf(File2, "%d", &x2);
}
}
//没有读完的继续
while (ret1 != EOF)
{
fprintf(Mfile, "%d\n", x1);
ret1 = fscanf(File1, "%d", &x1);
}
while (ret2 != EOF)
{
fprintf(Mfile, "%d\n", x2);
ret2 = fscanf(File2, "%d", &x2);
}
//关闭文件
fclose(File1);
fclose(File2);
fclose(Mfile);
}
注意:在从大文件读取数据时,要注意的是,fscanf读取数据成功返回的都是1,因此我们需增设两个变量接收读取到的值。
📌总体逻辑
- 造大文件
- 分别从大文件读取数据排序好后输入进file1和file2
- 将file1和file2合并为mfile
- 删除file1 file2 ,重命名mfile为file1,继续输入数据进file2
- 不断重复上述过程直到bigfile数据读取完毕
总体参考代码:
//创建大文件
void CreateData()
{
FILE* fin = fopen("data.txt", "w");
if (fin == NULL)
{
perror("fopen fail");
return;
}
const int N = 100;
srand(time(0));
//读入N个数 随机数
for (int i = 0; i < N; i++)
{
int num = rand() + i;
fprintf(fin, "%d\n", num);
}
fclose(fin);
}
//分别读入数据进file1和file2并且排序
int RDatatoSortFile(const char* file1,FILE* bigfile,int n)
{
int num = 0;
vector<int> v;
//注意先提前开空间 因为后面push_back是继续开空间!
FILE* file = fopen(file1, "w");
if (file == NULL)
{
perror("fopen fail");
return 0;
}
//从bigfile读取n个数据进数组
int j = 0;
for (int i = 0; i < n; i++)
{
if (fscanf(bigfile, "%d", &num) == EOF)
break;
v.push_back(num);
j++;
}
//数据读完提前返回
if (j == 0)
return 0;
sort(v.begin(), v.end());
//将数据读入file
for (int i = 0; i < j; i++)
{
fprintf(file, "%d\n", v[i]);
}
fclose(file);
//fclose(bigfile);
return j;
}
void mergeFile(const char* file1,const char* file2,const char* mfile)
{
FILE* File1 = fopen(file1, "r");
if (file1 == NULL)
{
perror("fopen fail");
return;
}
FILE* File2 = fopen(file2, "r");
if (file2 == NULL)
{
perror("fopen fail");
return;
}
FILE* Mfile = fopen(mfile, "w");
if (mfile == NULL)
{
perror("fopen fail");
return;
}
//fscanf读取数据成功都返回的是1 错误返回的是-1 所以要另外开两个变量接收
int x1 = 0;
int ret1 = 0;
ret1 = fscanf(File1, "%d", &x1);
int ret2 = 0;
int x2 = 0;
ret2 = fscanf(File2, "%d", &x2);
while (ret1 != EOF && ret2 != EOF)
{
if (x1 <x2)
{
//读进mfile
fprintf(Mfile, "%d\n", x1);
ret1 = fscanf(File1, "%d", &x1);
}
else
{
fprintf(Mfile, "%d\n", x2);
ret2 = fscanf(File2, "%d", &x2);
}
}
//没有读完的继续
while (ret1 != EOF)
{
fprintf(Mfile, "%d\n", x1);
ret1 = fscanf(File1, "%d", &x1);
}
while (ret2 != EOF)
{
fprintf(Mfile, "%d\n", x2);
ret2 = fscanf(File2, "%d", &x2);
}
//关闭文件
fclose(File1);
fclose(File2);
fclose(Mfile);
}
int main()
{
//CreateData();
FILE* bigfile = fopen("data.txt", "r");
if (bigfile == NULL)
{
perror("fopen faile");
}
const char* file1 = "file1.txt";
const char* file2 = "file2.txt";
const char* mfile = "mfile.txt";
int n = 0;
cin >> n;
//1.先分别将数据读入file1和file2
RDatatoSortFile(file1,bigfile,n);
RDatatoSortFile(file2, bigfile, n);
//2.将file1和file2内容合并进mfile
//3.将file1和file2删除 mfile改为file1
while (1)
{
mergeFile(file1, file2, mfile);
// 删除file1和file2
remove(file1);
remove(file2);
//重命名mfile为file1
rename(mfile, file1);
//归并mfile(file1)和fle2 判断是否读完
int num = 0;
num = RDatatoSortFile(file2, bigfile, n);
if (num == 0)
break;
}
fclose(bigfile);
return 0;
}
总结:本节我们学习了用于排序内存不能一次性处理的数据量的排序 --- 外排序,同时根据外排序的原理设计了文件归并排序。