首页 > 技术文章 > 败者树,图解败者树,算法执行调整每一步详细分析

meihao1203 2018-08-30 17:00 原文

多路归并-败者树。可以视为一棵安全二叉树的变形。每个叶子结点存放各归并段在归并过程中当前参加比较的记录,内部结点用来记忆叶子结点和对应的父结点比较的失败者,而胜者继续往上进行比较,一直到根结点,也就是0号节点。最终各节点就是一次遍历调整得到的最大值。
详细实现代码链接:  https://github.com/meihao1203/learning/blob/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E8%B4%A5%E8%80%85%E6%A0%91.cpp
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个数据就是错的

推荐阅读