23の機能(1億メモリ未満)の大規模ファイルの並べ替え
参照元:
大量のデータを含むディスクファイルの処理方法-プログラミングの技術
1考え
ここでは多方向マージが使用されます。これは、双方向マージの場合、最後の2つのファイルがどんどん大きくなり、メモリが不足するためです。ただし、マルチチャネルマージの場合、ファイルIOが原因で速度が低下します。
手順:
1)大きなファイルがバッチでメモリに読み込まれ、複数の小さなファイルに出力されます
。2)すべての小さなファイルが同時に開かれ、各ファイルの最初の値が事前にメモリ(配列)に読み込まれ、敗者ツリーが並べ替えに使用されます。 、一定の出力調整;
3)しかし、ここでは、敗者ツリーの最後のK-1番号を使用するという問題があります。すべてのファイルが読み取られるため、新しい番号エントリツリーがないため、K-1ツリーは使用されません。並べ替えの場合、すべてのデータよりも大きい値が処理に使用されます。つまり、最後のMAX値が並べ替え順序で各小さなファイルに入力されるため、KMAX値はこれらの値を強制的に出力できます。
2実装(多方向マージ、敗者ツリー)
#pragma warning(disable:4996)
#include <iostream>
#include <ctime>
#include <fstream>
#include <cassert>
#include<string>
using namespace std;
#define NUM (10000) //生成的数据量
#define MIN (-1) //数据节点的最小值,用于给内部节点初始化下标,方便首次可以比较形成败者树
#define MAX (10000000) //用于标识结束输出的一种方法
//使用败者树排序logK
typedef int* LoserTree; //ls:最小值ls[0]1个加上内部节点K-1共K个元素(存放的代表意思全是值的下标)
typedef int* DataStruct;//b: 叶子节点,即数据节点K个加上1个用于初始化的最小值(值)
//生成一万个数
int ProductNum() {
FILE *fp = fopen("UnSortFile.txt", "w");
assert(fp);
//给数据赋值
int *arr = new int[NUM];
for (int i = 0; i < NUM; i++) {
arr[i] = i;
}
//将数据打乱,模拟无序的数据
int i, j, k;
srand((unsigned)time(NULL));
for (k = 0; k < NUM; k++) {
i = (rand() * RAND_MAX + rand()) % NUM;
j = (rand() * RAND_MAX + rand()) % NUM;
swap(arr[i], arr[j]);//利用随机下标对数据打乱
}
//输出到文件中
for (k = 0; k < NUM; k++) {
fprintf(fp, "%d\n", arr[k]);
}
fclose(fp);
delete[] arr;
return 0;
}
//外排序实现归并海量文件--多路归并
class ExternalSort {
public:
ExternalSort(const char *unSortFile, const char *sortFile, int count) {
m_UnSortFile = new char[strlen(unSortFile) + 1];
strcpy(m_UnSortFile, unSortFile);
m_SortFile = new char[strlen(sortFile) + 1];
strcpy(m_SortFile, sortFile);
m_Count = count;
m_file = 0;
}
//外部可以调用的多路归并排序函数
void sort() {
ls = new int[m_file];
b = new int[m_file + 1];
time_t start = time(NULL);
MemorySort();
MergeSort();
time_t end = time(NULL);
time_t t = (end - start) * 1000.0 / CLOCKS_PER_SEC;
cout << "多路归并排序之后秒数为:" << t << endl;
delete[] ls;
delete[] b;
}
//从文件中读n个数字,返回具体读到的个数,遇到\n或者空格会跳过
int ReadNum(FILE *f, int *arr, int n) {
#if 0
int i = 0;
while (i < n) {
int ret = fread((int*)(arr + i), sizeof(int), 1, f); //返回读到的实际个数,fread不够会返回具体个数
if (ret <= 0) {
break;//读完返回
}
i++;
}
return i;
#endif
int i;
for (i = 0; i < n; i++) {
if (fscanf(f, "%d", &arr[i]) == EOF) {
//fscanf成功返回1,失败-1,读到文件尾EOF
break;
}
}
return i;
}
//写n个数进文件
void WriteNum(FILE *f, int arr[], int n) {
#if 0
//fwrite容易出bug
int ret = fwrite(arr, sizeof(int), n, f);
if (ret != n) {
cout << "写入临时文件错误." << endl;
return;
}
#endif
int i;
for (i = 0; i < n; i++) {
fprintf(f, "%d\n", arr[i]);//n-1该成n的话输出排序的小文件最后有个换行符
}
fprintf(f, "%d", MAX);//最后一个用1000000标志排序,用于表示输出到末尾
}
//字符串组合函数
string ConectStr(int i) {
string s1 = to_string(i);
string s2("tmp.txt");
return s1 + s2;
}
//比较函数
static int Compare_Int(const void *a, const void *b) {
return *(int*)a - *(int*)b; //返回int时不能用大于小于比较,具体原因我也不知道。。。
}
//大文件读到内存排序后输出到小文件
void MemorySort() {
FILE *fin = fopen(m_UnSortFile, "rt"); //t代表以文本方式打开
assert(fin);
int *arr = new int[m_Count]; //每次读n个数
int i = 0;
while (true){
int num = ReadNum(fin, arr, m_Count);
if (num <= 0) {
cout << "内存排序,文件读取完毕." << endl;
cout << "文件数为:" << m_file << endl;
break;
}
qsort(arr, num, sizeof(int), Compare_Int);
string tmpFile = ConectStr(i++);
FILE *fout = fopen(tmpFile.c_str(), "wt");
assert(fout);
WriteNum(fout, arr, num);
m_file++;
fclose(fout);
}
delete[] arr;
fclose(fin);
}
//归并有序小文件(Bug:末尾有个9在,应该是某个数被拆分了,并且文件被读完了,数组里还剩下99个,必须借用MAX逼出来才行,用下面那种排序吧,)
void MergeSort1() {
//打开要归并输入的新文件
FILE *fout = fopen(m_SortFile, "wt");
assert(fout);
//打开需要归并的所有小文件
int i = 0;
FILE **farray = new FILE*[m_file];
while (i < m_file) {
string tmpFile = ConectStr(i);
farray[i] = fopen(tmpFile.c_str(), "rt");
assert(farray[i]);
i++;
}
//先预读每个小文件的首个最小数进内存(叶子节点数组)
for (int i = 0; i < m_file; i++) {
if (fscanf(farray[i], "%d", &b[i]) == EOF) {
cout << "文件" << i << "没有内容可读," << "归并文件数不符合." << endl;
return; //归并文件数不符合直接退出
}
}
//开始使用败者树进行排序
CreateLoser(); //创建树会调整一次
while (true) {
//1输出最小数
fprintf(fout, "%d\n", b[ls[0]]);//排序后也会有一个换行符
//2再读进一个最小数
int min = ls[0];
int count = 0; //统计是否文件被读完而退出
while (true) {
//当前文件读完从下一文件读
if (min < m_file) {
if (fscanf(farray[min], "%d", &b[ls[0]]) == EOF) {
min++;
continue;
}
else {
break;
}
}
//否则从开始读到当前文件
else {
min = 0;
while (min < ls[0]) {
if (fscanf(farray[min], "%d", &b[ls[0]]) == EOF) {
min++;
count++;
continue;
}
else {
break;//读到或者所有文件读完而退出
}
}
//3所有文件都等于EOF则读完
if (count >= ls[0]) {
cout << "归并排序,所有文件读取完毕." << endl;
return;
}
else {
break;//读到则退出大while
}
}
}
Adjust(ls[0]); //再次将重新输入的下标调整
}
//清理
fclose(fout);
for (int i = 0; i < m_file; i++) {
fclose(farray[i]);
}
delete[] farray;
}
//归并有序小文件(小Bug:末尾有个\n在)
void MergeSort() {
FILE *fout = fopen(m_SortFile, "wt");
FILE* *farray = new FILE*[m_file];
//打开所有k路输入文件
for (int i = 0; i < m_file; i++) {
string tmpFile = ConectStr(i);
farray[i] = fopen(tmpFile.c_str(), "rt");
}
//初始读取
for (int i = 0; i < m_file; i++) {
//读每个文件的第一个数到data数组
if (fscanf(farray[i], "%d", &b[i]) == EOF) {
printf("there is no %d file to merge!", m_file);
return;
}
}
CreateLoser();
int index;
while (b[ls[0]] != MAX) {
//输出最小数
index = ls[0];
fprintf(fout, "%d\n", b[index]);
//再读一个最小数
fscanf(farray[index], "%d", &b[index]);
Adjust(index);
}
fprintf(fout, "%s", "数据排序完成.");
//fprintf(fout, "%d", b[ls[0]]);最后因为每一个临时文件都有MAX,所以最后的数据都是MAX,最小值也就是MAX了
fclose(fout);
for (int i = 0; i < m_file; i++) {
fclose(farray[i]);
}
delete[] farray;
}
//创建败者树
void CreateLoser() {
//1 初始化数据节点的最小值
b[m_file] = MIN;
//2 初始化内部节点记录的下标
for (int i = 0; i < m_file; i++) {
ls[i] = m_file;
}
//3 调整败者树
for (int i = m_file - 1; i >= 0; i--) {
Adjust(i);
}
}
//调整败者树 s一开始代表当前节点,实际是一直指向胜利者,即数据最小的值(与用于初始化的最小值MIN不一样)
void Adjust(int s) {
int tmp;
int t = (s + m_file) / (2); //叶子节点与内部节点建立关系,t代表当前节点s的父节点
while (t > 0) {
//t=0,即父节点是ls[0]节点时,证明该次排序调整结束,已经找到最小值了嘛
if (b[s] > b[ls[t]]) {
//新入树节点大于上一次的父节点,父节点记录新的失败者
tmp = s;
s = ls[t]; //s永远指向胜利者,即最小值
ls[t] = tmp; //父节点保存新的失败者
}
t = t / 2; //沿根节点上比较
}
ls[0] = s;
}
~ExternalSort() {
if (m_SortFile != NULL) {
delete[] m_SortFile;
m_SortFile = NULL;
}
if (m_UnSortFile != NULL) {
delete[] m_UnSortFile;
m_UnSortFile = NULL;
}
}
private:
char *m_SortFile;
char *m_UnSortFile;
int m_Count; //每次排序的个数,在内存读取时必须要
int m_file; //归并文件数
//败者树实现
LoserTree ls; //败者树 两者赋值时再排序时赋,个人建议拉出去当全局数组更好一点(看个人吧)
DataStruct b; //数组元素
};
/*
==================================================主函数测试==========================================
*/
/*
fread与fwrite输出新文件容易乱码的原因:
例如读进数组的数字23673(0x5c79,16进制),再写进新文件时该数字会转成ASCII码输出,但是输出时该数字0x5c79会被转成y\
因为0x5c=92对应ASCII的y,0x79=121对应\。 注:小端模式下
*/
int main() {
//指定大文件与排好序的输出文件名
ExternalSort sort("UnSortFile.txt", "SortFile.txt", 1000);
//排序
sort.sort();
return 0;
}