算法整理:外排序篇-多路平衡归并

计算机包含两种存储器:内部存储器和外部存储器。内存存取速度快,支持随机存取,但是容量小价格且昂贵;外存存取速度慢,但是容量大且价格便宜。

外部排序方法

外部排序分为两个步骤,首先按照可用的内存大小,将外存中的数据分段,然后将它们依次读入内存并进行排序,并将排序后的数据重新放入外存。最后将这些排序后的分段逐趟归并,直到所有数据有序。
第一步很简单,就是用前四篇博客讲的内部排序方法对分段后的数据依次排序。
第二步用到了归并排序方法,但是与内部排序中讲的归并排序不同,不仅涉及到内存的读写,还涉及到外存的读写。
所以有:
外 部 排 序 所 需 的 总 时 间 = 每 个 分 段 进 行 内 部 排 序 的 时 间 + 外 存 信 息 读 写 时 间 + 归 并 所 需 时 间 外部排序所需的总时间=每个分段进行内部排序的时间+外存信息读写时间+归并所需时间 =++由于外存读写比内存读写要费时的多,所以我们需要尽量减少外存信息读写的次数。那么外存读取次数与哪些因素有关呢?下面来进行分析。

假设外存中有n条记录,每次读写数据最多读取m条记录。
首先,在对外存数据分段后,需要将外存每个分段数据读入内存并进行内部排序,然后将排序后的分段写入外存,这里不可避免的需要对外存进行总共 n m \frac{n}m mn 次的读和同样次数的写。
然后,需要对排好序的分段进行归并。如果将待排序数据被分为10段,使用二路归并的方法进行两两归并,则需要进行4趟归并。其中每趟归并都需要对外存进行总共为 n m \frac{n}m mn 次的读和同样次数的写,流程如下图所示
在这里插入图片描述
所以,整个外部排序过程需要对外存进行 10 ∗ n m 10*\frac{n}m 10mn次读写。容易发现外存读写的次数与归并的路数、数据的分段数有关。若使用5路平衡归并,则仅需要进行两趟归并,减少了 4 ∗ n m 4*\frac{n}m 4mn次读写。若仍使用2路平衡归并,但是将数据分成4段,则也仅用两趟归并就能完成排序。
在这里插入图片描述

综上,减少外存读写的方案有两种,一是减少初始分段的个数,二是增加归并的路数。

多路平衡归并

相对于2路归并(详细内容参照算法整理:内排序篇-二路归并排序&线性时间排序),多路平衡归并增加了归并路数,从而减少了外存读写的次数,但是单纯地增加归并路数会增加内部归并的时间。如果使用2路归并,每得到一个归并后的记录仅需一次比较,而对于k路归并,每得到一个归并后的记录需要进行k-1次比较。k越大,内部归并所耗费的时间代价就越大。如果内部归并增加的代价大于或等于外存读取减少的代价就得不偿失了。
这里需要引入一种名为败者树的数据结构(详细内容参照数据结构:胜者树与败者树),败者树可以在 l g k lgk lgk次比较中选出k个数据中的最小值。这样能够有效地减少内部归并所带来的时间代价。

Tips:败者树的构建和调整
(下面内容假设你已熟悉败者树)
1. 败者树的调整
败者树的调整是一个自下向上的过程,新加入败者树的节点需要与它的父节点进行比较,败者留在父节点,而胜者向上传播,与上一层父节点比较,然后再向上传播…直到传播到根节点,调整结束。
2. 败者树的构建
令所有非叶子节点指向一个含有最小关键字的叶子节点,然后从各个节点出发调整败者树即可完成构建。

C++代码实例
本人水平有限,代码仅演示多路平衡归并的思路,并非最优写法,其中内排序算法使用STL自带的内省排序。

#include <random>
#include <time.h>
#include <vector>
#include <array>
#include <algorithm>
#include <fstream>
#include <string>
using namespace std;

//创建待排序文件  包含10万个随机生成的整数
void CreateFile() {
	ofstream out("test.txt");
	default_random_engine e(time(0));
	uniform_int_distribution<int> u(0, 100000);

	for (int i = 0; i < 100000; i++) {
		out << u(e) << endl;
	}
	out.close();
}

//多路平衡归并,用来排序整数
const int BuffSize = 10000;//假设最大缓存空间为10000
const int merge_K = 4;//归并的路数

//一开始使用的内排序算法,对10万条数据进行排序,生成10个各自包含10000条有序数据的文件
int InnerSort(string filepath) {
	ifstream in(filepath);
	ofstream out;
	vector<int> list;
	int filenum = 0;
	string temp;
	while (true)
	{
		while (list.size()< BuffSize ) {
			if (in >> temp) {
				list.push_back(stoi(temp));
			}
			else break;
		}
		if (list.size() == 0)break;
		sort(list.begin(), list.end());
		out.open("subsection_0_" + to_string(filenum) + ".txt");
		for (auto val : list)out << to_string(val) << endl;//写入数据
		out.close();
		list.clear();
		filenum++;
	}
	in.close();
	return filenum;
}

//败者树相关
int Adjust(array<int, merge_K-1> &lossertree, array<vector<int>::iterator, merge_K+1> inputbfIterators,int index) {
	int winner = index;
	index = index + merge_K - 1;
	while (index>0)
	{
		if (*(inputbfIterators[winner]) > *(inputbfIterators[lossertree[(index - 1) / 2]])) {
			int temp = winner;
			winner = lossertree[(index - 1) / 2];
			lossertree[(index - 1) / 2] = temp;
		}
		index = (index - 1) / 2;
	}
	return winner;
}

int CreateLosserTree(array<int, merge_K-1> &lossertree, array<vector<int>::iterator, merge_K+1> inputbfIterators) {
	int winner = 0;
	for (int i = lossertree.size() - 1; i >= 0; i--) lossertree[i] = merge_K;
	for (int i = 0; i < merge_K; i++) winner = Adjust(lossertree, inputbfIterators, i);
	return winner;
}

//K路平衡归并
void K_Merge_Sort(string filepath) {
	int filenum = InnerSort(filepath);
	const int buffersSize = BuffSize / (merge_K + 1);//merge_K个读入数据的buffer,1个写出数据的buffer
	array<vector<int>, merge_K> inputBuffer;
	vector<int> minval;
	minval.push_back(-1);
	array<vector<int>::iterator, merge_K+1> inputbfIterators;
	inputbfIterators[merge_K] = minval.begin();
	vector<int> outputBuffer;
	int mergenum = 0;
	string temp;
	while (filenum > 1)
	{
		cout << "第" << mergenum+1 << "轮归并" << endl;
		for (int i = 0; i < (filenum + merge_K - 1) / merge_K; i++) {
			array<ifstream, merge_K>reader;
			const int num_in_merge = ((i + 1)*merge_K < filenum) ? merge_K : (filenum % merge_K);//当前同时进行归并的段数
			for (int j = 0; j < merge_K; j++) {
				if (j < num_in_merge) {
					reader[j].open("subsection_" + to_string(mergenum) + "_" + to_string(i * merge_K + j) + ".txt");
					inputBuffer[j].clear();
					for (int k = 0; k < buffersSize; k++) {
						if (reader[j] >> temp)inputBuffer[j].push_back(stoi(temp));
						else break;
					}
				}
				else {
					inputBuffer[j].push_back(0x3f3f3f3f);
				}
				inputbfIterators[j] = inputBuffer[j].begin();
			}
			ofstream writer("subsection_"+ to_string(mergenum+1)+"_"+ to_string(i) + ".txt");
			outputBuffer.clear();
			array<int, merge_K-1> lossertree;
			int winner = CreateLosserTree(lossertree, inputbfIterators);
			int value = 0;
			while (true)
			{
				for (int j = 0; j < num_in_merge; j++) {
					if (inputbfIterators[j] == inputBuffer[j].end()) {
						inputBuffer[j].clear();
						for (int k = 0; k < buffersSize; k++) {
							if(reader[j] >> temp)inputBuffer[j].push_back(stoi(temp));
							else {
								inputBuffer[j].push_back(0x3f3f3f3f);
								break;
							}
						}
						inputbfIterators[j] = inputBuffer[j].begin();
					}
				}
				if (outputBuffer.size() >= buffersSize) {
					for (auto item : outputBuffer)writer << to_string(item) << endl;
					value += outputBuffer.size();
					outputBuffer.clear();
				}
				winner = Adjust(lossertree, inputbfIterators, winner);
				if (*(inputbfIterators[winner]) != 0x3f3f3f3f)outputBuffer.push_back(*(inputbfIterators[winner]++));
				else break;
			}
			if (outputBuffer.size() > 0) {
				for (auto item : outputBuffer)writer << to_string(item);
				value += outputBuffer.size();
			}
			cout << "文件  "<< "subsection_" + to_string(mergenum + 1) + "_" + to_string(i) + ".txt 归并完毕 " <<"数据数量"<<value << endl;
		}
		filenum = (filenum + merge_K - 1) / merge_K;
		mergenum++;
	}
}

int main()
{
	CreateFile();
	K_Merge_Sort("test.txt");
}

参考文献

《数据结构》(严蔚敏)
胜者树与败者树

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值