外排序与MapReduce的Sort

数据结构课设——外排序

Visual Studio 2019

Qt Creator4.9

C++

代码地址:DataStructureCourseProject/ExternalSort(Qt+Vs) at main · Tcoder-l3est/DataStructureCourseProject (github.com)

基础数据结构

使用竞赛树—最小书输者树来实现外排序的归并串的K路归并最终得到排序文件

基础要求
  1. 设计并实现最小输者树结构 ADT,ADT 中应包括初始化、返回赢者,重构

等基本操作。

  1. 应用最小输者树设计实现外排序,外部排序中的生成最初归并串以及 K 路

归并都应用最小输者树结构实现;

  1. 随机创建一个较长的文件作为外排序的初始数据;设置归并路数以及缓冲区

的大小;获得外排序的访问磁盘的次数并进行分析。可采用小文件来模拟磁

盘块。

基本步骤
  1. 部分排序生成顺串
  2. 顺串进行归并(可能多次 可能多路归并)
最小输者树
  • 首先定义Player结构体,包括元素值以及串号,重载运算符

    struct Player
    {//选手
    	int element;//元素值 
    	int num;//顺串号
    	//bool operator<(const Player &p1)
    	//{//先看顺串号再看元素
    	//	return (num != p1.num) ? (num < p1.num) : (element < p1.element);
    	//}
    	bool operator<=(const Player& p1)
    	{//先看顺串号再看元素
    		return (num != p1.num) ? (num < p1.num) : (element < p1.element);
    	}
    	Player& operator=(const Player& p)
    	{
    		num = p.num;
    		element = p.element;
    		return *this;
    	}
    	friend ostream& operator<<(ostream& out, Player& p) {
    		out << p.element;
    		return out;
    	};
    };
    
  • 定义最小输者树类

    
    template<class T>
    class minLoserTree
    {
    public:
    	minLoserTree(T* ThePlayers = NULL, int n = 0);
    	~minLoserTree() { delete[] tree; delete[] edges; }
    
    	void initialize(T* thePlayer,
    		int theNumberOfPlayers);
    	void play_once(int p, int leftChild, int rightChild);
    	int winner(int x, int y) //返回更小的元素下标
    	{ //winner 入边 
    		return players[x] <= players[y] ? x : y;
    	};
    	int loser(int x, int y)//返回更大的元素下标
    	{//loser 入内部节点
    		return players[x] <= players[y] ? y : x;
    	};
    	void replay(int thePlayer);//重构
    	void output();
    	void maintain() { tree[0] = edges[1]; };//维护tree[0]
    	long long int top() {//返回最小的tree[0]
    		return tree[0];
    	};
    	void make_run(fstream& fin);
    	void merge_K(int k, int sta, int level);
    	void get_total() {
    		totals = 0;
    		if (max_run % kk == 0) {
    			int temp = max_run / kk; totals += temp;
    			while (temp != 1)
    			{
    				temp /= kk;
    				totals += temp;
    			}
    		}
    		else
    		{
    			int temp = max_run / kk; totals += temp; totals++;
    			while (temp != 0)
    			{
    				temp /= kk;
    				totals += temp;
    				totals++;
    			}
    		}
    	};
    	int kk;				  // K
    private:
    	T* players;//元素数组
    	int* edges;//边上的晋级的元素
    	int numberofPlayers;//总共多少参赛者
    	int* tree;//专门记录内部节点,tree[0]用来记录最后的输者
    	int lowExt;           // lowest-level external nodes
    	int offset;           // 2^log(n-1) - 1
    	int totals = 0;           //K路合并总共多少 S
    
    };
    
  • image-20220406200440650

  • 首先是私有成员:player是存具体元素的(Player 结构体)

  • edges是需要记录 输者树边的信息,边上的信息是记录的晋级的元素,也就是赢的元素,大的元素

  • t是记录树的内部节点,存的是顺串号

  • lowExt 是最底层叶子叶节点的位置

  • offset 是用来看每一层多少节点的

image-20220406201103144

拓展之模拟磁盘

结构体Page

//缓冲区 
struct Page {
	long int* arr;
	int position; //当前顺串扫描的位置 
	Page() {
		position = 0;
	}
	Page(int page_size) {
		position = 0;
		arr = new long int[page_size + 1];
	}
};

主要用在K路归并的函数里面

其实是复杂化了,就是有个输出缓冲区,由Page构成,每次先输入到缓冲区里面,到缓冲区满了就写入到磁盘,也就是真是的文件,然后清空缓冲区。

其中还得考虑一些模拟指针等等问题

我实现的貌似还有小BUG~

其他

自己创建数据集,这个很简单了就是随机数~

设计思路

输者树数据结构相关
  • voidminLoserTree<T>::initialize(T* thePlayer, int theNumberOfPlayers)设计思路

    进行最小输者树的初始化,根据传入的数组,计算得出最底层外部节点个数,单独处理之后,再对上一层进行处理(比赛一次),如果是奇数个,则需要特殊处理。之后进行play_once()

  • void minLoserTree<T>::play_once(int p, int leftChild, int rightChild)设计思路

    根据传入的左右孩子,先进行左右孩子的比赛,并且输者放在tree树结构中,赢者放在边结构中,之后,只要没到根节点,就一直比赛,循环构建好整棵树。

  • void minLoserTree<T>::replay(int thePlayer)设计思路:

    首先根据被替换掉的选手的索引,判断是最外层节点,还是上一层的节点,然后对于从此节点到根的比赛,进行重比,即完成了重构, O ( log ⁡ ( n ) ) O(\log(n)) O(log(n))

外排序相关
  • void minLoserTree<T>::make_run(fstream& fin)的设计思路

    进行生成顺串的操作。

    首先从文件、外存中读入输入缓存区大小的文件。

    之后进行根据输入的元素进行内部排序,使用最小输者树进行,方法为:输入P个元素初始化一个最小输者树,顺串号都为1,之后将树顶,即最小的元素W加入到输出缓冲区中,然后,用新的元素N替代最小元素,如果新输入的<输出的,则N的顺串号=W顺串号+1,否则N顺串号=W顺串号,N代替W,重构输者树;

    一直进行,如果输出满了就输出到外存。直到外存中所有元素都完成顺串的生成。

  • void minLoserTree::merge_K(int k, int sta, int level)的设计思路

    K路归并

    对生成的顺串进行K路合并:

    因为K可以随机指定,不是固定为顺串数目,所以合并树可能是多层。

    首先:需要确定层数,如果是顺串层,则需要读入顺串,然后生成中间的合并的文件Si,之后对于S层 需要再向上进行K路合并,一直到最后只剩下一个S为止,也就是最终排序好的文件T。

    所以上述过程是一个迭代过程,确定好每次迭代量:即合并的文件,生成的文件,进行K路合并即可。

    而具体K路合并过程:根据当前需要合并的文件数目K,建立K个选手组成的最小输者树,然后对应每个文件,输入元素,进行建树,之后输出最小的元素,再用下一个元素代替他,重构,直到每个文件都处理完,把输出存到一个新文件中。

    image-20220406201505421

磁盘模拟相关
  • Page* make_in_Buffer(int now_k)设计思路

    生成一个输入缓冲区,每次根据当前的合并的顺串数目进行K路合并。所以需要用每次新建一个page类型的数组,用来模拟分页式缓存区。

    由上述可知:

    需要自定义struct Page类型,作为页,即缓存区为 Page数组,每一个数组元素都是一页,并且需要加一个位置指针position,来记录读写到的位置。

  • void disk_in_buffer(int now_k, ifstream* fins, Page* inBuffer)思路

​ 从磁盘输入模拟缓存区,只需要打开文件然后把缓存区输满,或者把文件输空。

  • void buffer_to_disk(int outpoint,ofstream &fout, vector<long> outBuffer)函数的设计思路

    如果输出缓冲区已经满了,此时需要调用该函数,把缓冲区的数据输入到磁盘中,访问一次磁盘,并且清空缓存区(或者直接覆盖)

    利用PAGE数组,因为磁盘文件可能是多个,所以传入的是ifstream输入流数组,进行多个文件的输入,并且写到inbuffer中。

思考

  1. 进行大数据测试时,排序时间较长,有时甚至能长达10+min

    解决:

    多线程外排序解决大数据排序问题(并行内排和并行归并)

    并行内排:

    采用分区的思想,同时启动多个外排序程序用来生成顺串,固定区大小,例如排1e6 每次读入1e5个数据,所以开启十个进程,分别读外存的0-1e5 1e5-2e5 2e5-3e5 ··· 9e5-1e6 同时进行,则可以以原来十分之一的速度生成所有的顺串。

    并行归并:

    多个线程单独对两两顺串进行合并,直到剩下一个。

    上述在不严谨的模拟下,大文件排序可以缩短4-5倍。

  2. 究竟为什么使用最小输者树,可以减少顺串个数?

    回答:

    读入新的数据,比原来的小 顺串号++ 大则不加

    一开始的输入时 sizeof_inbuffer个player sizeofile(文件大小,假设为10000) / INbuffer(输入缓冲区大小 假设为1000) =N,假设此时为10

    1000个元素,一定有一个最小值,他们都会输出到顺串1,

    最差情况 后面重构是每次都小一点点 则 后面也是最多N-1个

    则最多是N个顺串!!!

    最好的情况 则是如果是有序的 那么就只有一个顺串!!! 直接有序!

    而 只要前面数字有比较小的 则必然小于这个N

    而N则是其他排序方法的固定的顺串数目!

    随机数据的话 根据概率应该在中间附近 所以 平均顺串数目应该是N/2

    顺串的平均长度也比一般的内排序方法长,大概为2*players

    总之,动态确定顺串号,比其他排序有天然的有时,减少了一半的顺串数!

  3. 为什么用输者树不用赢者树?

    回答:

    排序需要不断重构树结构,输者树向上重构,只需要比较自己和父节点的大小就可以了,而赢者树需要看兄弟节点的大小,访存代价大。所以使用输者树是一个更优的选择!

  4. 归并的拓展

    借鉴霍夫曼树的思路,如果先合并的顺串长度最长,那么,在合并树中他出现的次数会比其他的多,代价会更高。所以记录每个顺串的长度,从最短的顺串开始合并。效率会更高。

  5. 外排序所需要的时间=内排序所需的时间+外存读写所需时间+内归并所需时间,减少外存信息的读写次数是提高外部排序效率的关键。

    而对于同一个文件来说,进行外排序所需读写外存的次数与归并趟数有关,为了减少归并趟数,可以通过减少初始顺串的个数、增加归并的路数两个方面进行。

部分测试

image-20220406202541271

image-20220406202620153

image-20220406202626654


MapReduce’s Sort

排序时机以及过程

map处理完数据送给Reduce之前进行处理的

过程

  1. 中间数据会在本地一个或者几个文件中,会对这些文件的内部记录进行一次快速排序
  2. 当本地文件快排完成之后,会对这些文件做归并排序,并且输入到一个大的文件中

image-20220406204357520


Java数据结构与方法

sort过程中,输出的中间文件会被拷贝到本地,生成一个或者几个segment类,Segment封装了中间数据以及操作。

同时系统启动两个Merge线程,一个对内存的segment归并,一个对硬盘的归并。

Merge类的merge方法生成了MergeQueue类的实例,调用了该类的merge方法,他的父类是PriorityMerge类,实际就是建立小根堆然后归并排序。

比的是Key

默认从小到大。


TotalOrderPartition方法

  1. 随机采样,预读一小部分
  2. 对采样数据排序
  3. 均分,假设N个Reducer,则取N-1个分割点
高效的划分模型

若Key 的数据类型是BinaryComparable的,即两个对象可以直接按字节比较大小(如Text),则以key构造Trie Tree;否则以二分查找来确定key的所属区间

Trie树(Prefix Tree)介绍_神奕的博客-CSDN博客_prefix tree

序。

比的是Key

默认从小到大。


TotalOrderPartition方法

  1. 随机采样,预读一小部分
  2. 对采样数据排序
  3. 均分,假设N个Reducer,则取N-1个分割点
高效的划分模型

若Key 的数据类型是BinaryComparable的,即两个对象可以直接按字节比较大小(如Text),则以key构造Trie Tree;否则以二分查找来确定key的所属区间

Trie树(Prefix Tree)介绍_神奕的博客-CSDN博客_prefix tree

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐