排序分为内部排序和外部排序两大类,若要排序的元素很多,整个序列的排序过程不可能在内存中完成,则此类排序就称为外部排序。针对外部排序这种问题,下面我们来介绍一种方法。
由于要排序的数据量很大,不可能一次性加载到内存,所以我们就采取"分而治之"的方法。从大文件中读取一部分数据加载到内存进行快速排序,再将有序的序列再放回到小文件中。最后再使用归并排序的思想,对这些小文件进归并处理,使得最后只剩下一个文件。则最后的这个文件里面记录的就是排好序的数据。
算法流程:
举个栗子:
问:有40亿个整数,只有1G的内存,对这些整数进行排序。
分析:
40个整数有160亿个字节,也就是16G的内存才能放下,所以直接加载到内存中排序是不现实的,我们可以将这40亿个整数放到20个小文件中,平均每个小文件是0.8G大约800M左右。我们分别将每个文件都加载到内存中对他们使用快速排序,然后再将排序好的文件写入到文件中。等到这20个小文件都全部有序的时候,我们再对这些小文件进行归并排序,最后将这20个小文件合并成一个文件,这样就将这40亿个整数排好序了。
代码实现:
#pragma once
#include<vector>
#include<cassert>
#include<cstdio>
#include<stdlib.h>
#include<algorithm>
using namespace std;
class BigDataSort
{
public:
BigDataSort(char * bigfile, int n, size_t limit) //n表示要切分成多份少小文件
:_bigfile(bigfile) //bigfile表示要排序的文件名
, _limit(limit) //_limit代表小文件中最多存放的数据个数
{
_SmallFileName.resize(n);
for (int i = 0; i < n;++i) //先创建n个文件名
{
char name[128] = {0};
_itoa(i,name,10);
_SmallFileName[i] = name;
}
}
void Sort()
{
SmallFileSort(); //小文件排序
SmallFileMerge(); //小文件归并
}
protected:
void SmallFileSort() //将大文件分成多份,排好序之后再写入小文件
{
FILE *fRead = fopen(_bigfile, "rb"); //打开大文件
assert(fRead);
for (size_t i = 0; i < _SmallFileName.size(); ++i)
{
vector<int> v;
for (size_t count = 0; count < _limit; ++count) //将这些小文件中的数据加载到内存
{
int num = 0;
if (_GetNum(fRead, num)) //如果读取成功,则将它放到vector中
v.push_back(num);
else
break;
}
//对这些数据进行快速排序
sort(v.begin(),v.end());
//将这些数据再写到文件中
FILE *fWrite = fopen(_SmallFileName[i].c_str(), "wb");
assert(fWrite);
for (size_t i = 0; i <v.size(); ++i) //将这些小文件中的数据加载到内存
{
_InputNum(fWrite,v[i]); //向文件中写入一个数
}
fclose(fWrite);
}
fclose(fRead);
}
//对这些文件进行归并排序
void SmallFileMerge()
{
while (_SmallFileName.size()>1) //将这些小文件最后合并的只剩一个文件
{
vector<string> v;
while (_SmallFileName.size() > 1)
{
string filename1 = _SmallFileName.back();
_SmallFileName.pop_back();
string filename2 = _SmallFileName.back();
_SmallFileName.pop_back();
FILE *fRead1 = fopen(filename1.c_str(), "rb");
assert(fRead1);
FILE *fRead2 = fopen(filename2.c_str(), "rb");
assert(fRead2);
string newfile = filename1 + filename2; //创建一个新的文件名,用来存放这两个文件合并后的结果
v.push_back(newfile); //将新文件保存起来
FILE* fWrite = fopen(newfile.c_str(), "wb");
assert(fWrite);
_Merge(fRead1,fRead2,fWrite); //对这两个文件中的数据进行归并排序
fclose(fRead1);
fclose(fRead2);
fclose(fWrite);
remove(filename1.c_str()); //删除这些已经合并过的辅助文件
remove(filename2.c_str());
}
for (size_t i = 0; i < v.size(); ++i)
{
_SmallFileName.push_back(v[i]);
}
}
//将最后合并的文件再拷贝回原文件中
FILE* fWrite = fopen(_bigfile,"wb");
assert(fWrite);
string filename = _SmallFileName[0];
FILE* fRead= fopen(filename.c_str(), "rb");
assert(fRead);
string line;
while (_GetLine(fRead,line))
{
line += '\n';
fputs(line.c_str(), fWrite);
line.clear();
}
fclose(fWrite);
fclose(fRead);
remove(filename.c_str()); //删除辅助文件
}
protected:
void _Merge(FILE* fRead1,FILE* fRead2,FILE* fWrite) //将小文件进行合并
{
int num1 = 0;
int num2 = 0;
bool file1 = _GetNum(fRead1,num1);
bool file2 = _GetNum(fRead2,num2);
while (file1!=false&&file2!=false) //归并排序
{
if (num1<num2)
{
_InputNum(fWrite,num1); //向文件中写入一个数
file1 =_GetNum(fRead1, num1);
}
else
{
_InputNum(fWrite, num2);
file2 = _GetNum(fRead2, num2);
}
}
while (file1==true)
{
_InputNum(fWrite, num1); //向文件中写入一个数
file1 = _GetNum(fRead1, num1);
}
while(file2==true)
{
_InputNum(fWrite, num2); //向文件中写入一个数
file2 = _GetNum(fRead2, num2);
}
}
protected:
bool _GetNum(FILE *fRead,int& num) //读取一个整数,并将整数通过num返回
{
int ch = 0;
string line;
while ((ch = getc(fRead)) != EOF&&ch != '\n')
{
line += ch;
}
if (ch == EOF)
return false;
else
{
num = atoi(line.c_str());
return true;
}
}
bool _GetLine(FILE* fRead,string& line) //读取一个整数,将整数以转化成字符串用line返回
{
int ch = 0;
while ((ch = getc(fRead)) != EOF&&ch!= '\n')
{
line += ch;
}
if (ch == EOF)
return false;
else
return true;
}
void _InputNum(FILE *fWrite,int num) //向文件中写一个整数,转化成字符串写入
{
char buf[120] = { 0 };
string line;
line = _itoa(num, buf, 10);
line += '\n';
fputs(line.c_str(), fWrite);
}
private:
vector<string> _SmallFileName; //存放小文件名的数组
char* _bigfile; //记录要排序的文件名
size_t _limit; //记录小文件的最多存放数据个数
};