23功能之海量文件(内存不足100M)的排序

23功能之海量文件(内存不足100M)的排序

参考自:
如何处理大数据量的磁盘文件-程序用编程艺术

1 思想
这里使用了多路归并,因为二路归并时,由于最后的两个文件变得越来越大,导致内存还是不满足。但多路归并时会因文件IO而变得慢。
步骤:
1)大文件分批读到内存排序后输出到多个小文件中;
2)同时打开所有小文件,预先读每个文件的第一个值进内存(数组),使用败者树排序,不断输出调整;
3)但是这里有一个问题,就是使用败者树剩下最后的K-1个数,由于所有文件读完了,没有新的数进树,所以那K-1个树不会排序,这里使用一个大于所有数据的值去处理,就是在排完序的每个小文件中最后输入一个MAX值,这样K个MAX值就能将这些值逼出来。

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;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值