23の機能(1億メモリ未満)の大規模ファイルの並べ替え

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;
}

おすすめ

転載: blog.csdn.net/weixin_44517656/article/details/108013718