多路归并-败者树。可以视为一棵安全二叉树的变形。每个叶子结点存放各归并段在归并过程中当前参加比较的记录,内部结点用来记忆叶子结点和对应的父结点比较的失败者,而胜者继续往上进行比较,一直到根结点,也就是0号节点。最终各节点就是一次遍历调整得到的最大值。
int arr[DataSourceNumber][2] = {
{10,9},{9,8},{11,7},{8,6},{8,7}
}; //假定有这个数据集,5个数据来源,每个数据来源有两个数据
对应到现实问题中,数据来源可以是一次不能全部读入计算机内存的文件。
算法核心,调整败者树 void adjust(int index) //败者树从叶子结点index开始向上调整
{
//找到index对应的父结点
int father = (index + DataSourceNumber)>>1;
while(father>0)
{
if( get(treeNode[father])>get(index) )
//和父结点比较,败者的索引更新父结点
{
int tmp = treeNode[father];
treeNode[father] = index;
index = tmp; //向上继续比较,更新index
}
father /= 2;
}
treeNode[0] = index;
}
|
//这个树和堆排序一样,只是想象出来的,真正排序的时候不会构建这么一个树 //初始化的过程中,在所有分组中查看第一个数据,找出最大的一个数据所在的分组进行初始化树的非叶子结点。上图中最大的是11,在第二号分组。 |
//初始化第一步之后,开始从最后一个叶子结点调整败者树,最终treeNode[0]存放的就是最大的一个数,也就是最终胜出的那个数。 //从4号开始,父结点2(11)>4(8),对应的父结点改成8所在的数据源4,相应的index=2;继续比较,没有发生变化; |
//3号的8开始比较,当比较到父结点的父结点的时候,满足败者更新父结点,index = 2。继续比较,最后treeNode[0] = 2; //2(11)调整没有变化. |
//1号的9调整之后,父结点更新变成1,index = 2;继续调整没有变化 | //0号的10和父结点指向的3中的第一个元素比较,不满足调整,接着和父结点的父结点比较,2(11)>0(10)调整,此时index = 2,表示胜利的结点在2号数据源,最终treeNode[0] = 2。输出最大值11 |
//输出11之后2号数据源剩下7,从该叶子结点开始重新调整 | //败者叶子结点,更新父结点,index = 1;继续和父结点的父结点1(0)比较 //0(10)>1(9),更新,index = 0;最终treeNode[0] = 0;输出第二大的值10,如下图。 |
//往后继续从0(9)叶子结点开始调整,最终输出9 | //9输出后,0号数据源没有数据了,可是下一步还要从这个打乱平衡的点继续调整,为了能够让算法继续进行下去,这里我们要进行特殊设置,该数据源如果没有数据了,就设置一个能表示的最小数据放在对应的点。 网上好多算法没有这个,其实败者树是用来处理海量数据的,所以一般不存在这样的问题,但是为了算法的鲁棒性和稳定性,讲道理还是要做设置的。 |
此时index = 3;1(1)>3(8),更新,index = 1 |
//treeNode[0]=1,输出9 |
//后面的操作类似,继续从1(8)开始调整,最终输出8,就不在画图说明了 | 输出11,10,9,9,8 |
//模拟一个小范围的败者树多路归并
//假设数据源来自5个数组,且这些数组都要是有序的(降序的)
#include<iostream>
#include<vector>
#include<limits>
using namespace std;
static const int DataSourceNumber = 5; //数据源有五个数组
//定义数据源,一共5个,假设每个数组有两个元素
int arr[DataSourceNumber][2] = {
{10,9},{9,8},{11,7},{8,6},{8,7}
};
//定义非叶子结点数组,非叶子结点的值表示该结点记录的是哪一个数组来源的值
int treeNode[DataSourceNumber];
//定义叶子结点数组,对应的数组小标表示是那个数据源,值表示数据源中的数据
int node[DataSourceNumber];
//定义数据源的偏移指向数据,因为每次从数据源中读取了一个数据,对应的索引就要偏移到下一位
int Iterator[DataSourceNumber] = {0}; //初始化全为0,因为每个数源都是要从第0个数据开始取
//定义获取叶子结点的值的函数
int get(int index)
{
return node[index];
}
//定义设置叶子结点的值的函数
void set(int index)
{
if(Iterator[index] == 2)
{
node[index] = numeric_limits<int>::min();
return ;
}
node[index] = arr[index][Iterator[index]] ; //结点node[index]的值就是index个数据源的第Iterator[index]个值,
//之后要设置Iterator[index]偏移到下一个,这样下次才能取出下一个值设置对应的node结点
Iterator[index] += 1;
}
void adjust(int index) //败者树从node结点index开始向上调整
{
//找到index对应的父结点
int father = (index + DataSourceNumber)>>1;
while(father>0)
{
if( get(treeNode[father])>get(index) )
//和父结点比较,败者的索引更新父结点
{
int tmp = treeNode[father];
treeNode[father] = index;
index = tmp; //向上继续比较,更新index
}
father /= 2;
}
treeNode[0] = index; //最终胜者
}
|
//设置摆着树的初始化工作
void init()
{//败者树要找出最小的指定个数的数,先用数据源数组中首个元素最大的数据源索引来初始化非叶子结点
//然后开始调整
memset(Iterator,0,sizeof(Iterator)); //
//1、设置node
for(int idx=0;idx!=DataSourceNumber;++idx)
{
set(idx);
}
int MAX = 0; //初始最大元素来自数组源0
for(int idx=0;idx!=DataSourceNumber;++idx)
{
if(node[idx]>node[MAX])
MAX = idx;
}
//2、初始化treeNode
for(int idx=0;idx!=DataSourceNumber;++idx)
{
treeNode[idx] = MAX;
}
//3、开始从最后一个非叶子结点调整
for(int idx=DataSourceNumber-1;idx>=0;--idx)
{
adjust(idx);
}
}
vector<int> merge(int cnt) //要输出cnt多少个值
{
vector<int> ans;
while(cnt)
{
ans.push_back(node[treeNode[0]]); //败者树的0号节点存放的就是当前最小的node值索引
set(treeNode[0]); //输出一个,要更新对应的叶子结点的值
adjust(treeNode[0]); //调整
--cnt;
}
return ans;
}
int main()
{
init();
vector<int> ret = merge(5);
for(auto elem:ret)
cout<<elem<<" ";
cout<<endl;
init();
vector<int> ret2 = merge(10);
for(auto elem:ret2)
cout<<elem<<" ";
cout<<endl;
system("pause");
}
//set函数没有这是min的操作,输出10个数据就是错的
|