椋鸟数据结构笔记#11:排序·下


萌新的学习笔记,写错了恳请斧正。

外排序(外部排序)

数据量非常庞大以至于无法全部写入内存时,我们应该怎么排序这些数据呢?

这时,就要学会直接进行在外存中排序,也就是外部排序。

外排序不是一种独立的排序算法,还是要依赖于之前的那些排序方式。

其基本思想是这样的:

  • 将庞大的数据拆分为一个一个子文件,使得每一个子文件的数据量在内存排序可承受的范围内。
  • 将每一个文件进行正常的排序,可以选用之前学的任何排序方法。
  • 将排序好的文件再进行归并,最终合成为包含有序的所有数据的文件。

看起来不是很复杂,但是归并部分还是很有东西的。下面我们逐步讲解:

文件拆分并排序

这并不复杂,比方说我们将文件拆分为splitCount个:

void FileSort(const char* filename, int splitCount)
{
	clock_t start = clock();

	//分割文件
	FILE* fp = fopen(filename, "r");
	if (fp == NULL)
	{
		perror("fail to open");
		exit(EXIT_FAILURE);
	}
	int* num = (int*)malloc(sizeof(int) * N / splitCount);	//存放每个分割文件的数据
	int iNum = 0;	//num数组的下标
	int iFile = 0;	//分割文件的下标
	int n = 0;	//读取的数据
	char splitFileName[50] = { 0 };	//分割文件名
	while (fscanf(fp, "%d", &n) != EOF)
	{
		if (iNum < N / splitCount - 1)
		{
			num[iNum++] = n;
		}
		else
		{
			num[iNum] = n;
			//排序
			HeapSort(num, N / splitCount);
			//写入文件
			sprintf(splitFileName, "SubSort\\sort_split%d.txt", iFile++);
			FILE* splitFile = fopen(splitFileName, "w");
			if (splitFile == NULL)
			{
				perror("fail to open");
				exit(EXIT_FAILURE);
			}
			for (int i = 0; i < N / splitCount; i++)
			{
				fprintf(splitFile, "%d\n", num[i]);
			}
			fclose(splitFile);
			printf("已分割第%d个文件,总共需分割%d个文件。\n", iFile, splitCount);
			iNum = 0;
		}
	}

	//归并文件
    //这里插入归并文件的代码

	//关闭文件
	fclose(fp);

	clock_t end = clock();

	printf("排序完成,共%d个数据,分割为%d个文件,归并为1个文件。\n", N, splitCount);
	printf("耗时:%f秒。\n", (double)(end - start) / CLOCKS_PER_SEC);
}
归并文件

这是重点也是最复杂的地方。

两个文件归并

我们知道两个排序文件归并的方法:两边一起读取,一直对比两边读取的数,不断取小写入输出文件即可。

void MergeFile(char* file1, char* file2, char* mergeFileName)
{
	FILE* fp1 = fopen(file1, "r");
	FILE* fp2 = fopen(file2, "r");
	if (fp1 == NULL)
	{
		perror("fail to open");
		exit(EXIT_FAILURE);
	}
	if (fp2 == NULL)
	{
		perror("fail to open");
		exit(EXIT_FAILURE);
	}
	FILE* fin = fopen("tmp.txt", "w");
	if (fin == NULL)
	{
		perror("fail to open");
		exit(EXIT_FAILURE);
	}
	int n1, n2;
	int ret1 = fscanf(fp1, "%d", &n1);
	int ret2 = fscanf(fp2, "%d", &n2);
	while (ret1 != EOF && ret2 != EOF)
	{
		if (n1 < n2)
		{
			fprintf(fin, "%d\n", n1);
			ret1 = fscanf(fp1, "%d", &n1);
		}
		else
		{
			fprintf(fin, "%d\n", n2);
			ret2 = fscanf(fp2, "%d", &n2);
		}
	}
	while (fscanf(fp1, "%d", &n1) != EOF)
	{
		fprintf(fin, "%d\n", n1);
	}
	while (fscanf(fp2, "%d", &n2) != EOF)
	{
		fprintf(fin, "%d\n", n2);
	}
	fclose(fp1);
	fclose(fp2);
	fclose(fin);
	remove(file1);
	remove(file2);
	rename("tmp.txt", mergeFileName);
}

注意:这里我们先写入数据至tmp.txt中,最后在将其改名为mergeFileName是为了防止mergeFileName就是file1或者file2的情况(也即直接把file2归并到file1中而不创建新的文件,或者反过来)。

多文件归并

但是我们往往不仅仅要把数据切分为两个,而是几十个上百个。这时我们就需要使用多文件归并的方法。

  1. 直接逐个归并

    这是最容易理解也是最差的方法。就是把拆分文件1和拆分文件2归并,结果再和拆分文件3归并,结果再和拆分文件4归并……最终得到完整的归并文件。

    傻子都能看出来这个方法的不靠谱,但是我们还是将代码贴出来:

    //归并文件,这段代码填入上方FileSort函数中归并的留空位置
    char* mergeFileName = "sort_merge.txt";
    char file1[50] = "SubSort\\sort_split0.txt", file2[50] = {0};
    for (int i = 1; i < splitCount; i++)
    {
    	sprintf(file2, "SubSort\\sort_split%d.txt", i);
    	MergeFile(file1, file2, mergeFileName);
    	strcpy(file1, mergeFileName);
    	printf("已归并%d个文件,总共需归并%d个文件。\n", i + 1, splitCount);
    }
    

    这个方法是真的不靠谱,一亿个数据半小时还没归并完。

  2. 分治归并

    就是不断地两两分组归并,比方说9至16个文件归并成5至8个,5至8个归并位3至4个,再归并为2个,最后合成完整的排序后文件。

    //分治归并文件,这段代码填入上方FileSort函数中归并的留空位置
    MergeFileR(0, splitCount - 1);
    remove("SubSort\\sort_split0.txt");
    

    其中MergeFileR的定义如下:

    void MergeFileR(int left, int right)
    {
    	if (left >= right)
    	{
    		return;
    	}
    	int mid = (left + right) / 2;
    	//递归归并
    	MergeFileR(left, mid);
    	MergeFileR(mid + 1, right);
    	//归并
    	char file1[50] = { 0 }, file2[50] = { 0 }, mergeFileName[50] = { 0 };
    	sprintf(file1, "SubSort\\sort_split%d.txt", left);
    	sprintf(file2, "SubSort\\sort_split%d.txt", mid + 1);
    	sprintf(mergeFileName, "SubSort\\sort_split%d.txt", left);
    	MergeFile(file1, file2, mergeFileName);
    	printf("已归并%d与%d文件至%d。\n", left, mid + 1, left);
    }
    

    这个方法要靠谱一点,测试如下:

优化

进一步优化可以将基础操作从两个文件归并变成3个文件归并或者更多,这样分治时就是3合一或者更多。这样多路归并会增加内存开销和内部时间开销,但是会大大减少外部读写的开销。

更进一步优化可以引入败者树、最佳归并树等,我还不会。

  • 30
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

椋鸟Starling

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值