胜者树和败者树都是完全二叉树,是树形选择排序的一种变型。每个叶子结点相当于一个选手,每个中间结点相当于一场比赛,每一层相当于一轮比赛。
不同的是,胜者树的中间结点记录的是胜者的标号;而败者树的中间结点记录的败者的标号。
胜者树与败者树可以在log(n)的时间内找到最值。任何一个叶子结点的值改变后,利用中间结点的信息,还是能够快速地找到最值。在k路归并排序中经常用到。
叶子节点相当于参赛选手,中间节点是比赛,比赛中败者记录在中间节点,胜者继续参加后面的比赛,直到根节点。根节点之上的一个节点用来记录最终胜者。
败者树的建立:在参赛者数组b[]的最后添加一位,存放一个参赛选手的绝对最小值(选手都是正数的话,如-1)。所有中间节点都记录这个最小值的下标。然后依次调整(adjust())各个选手即可。
败者树的调整:当改变一个选手的值,需要调整以维持败者树的形态。败者树只需调整选手的父节点即可。当子节点的值大于父节点,则子节点记录于父节点(小为胜利,记录败者),父节点继续与其父节点比赛;若子节点小于父节点,则直接使用子节点进行下一轮比赛。
#define LEN 5//败者树容量,多路归并数目 #define MIN -1//所有数据的可能最小值 int ls[LEN+1];//败者树,ls[0]存放胜者,其余存放败者 int buf[LEN+1];//存放多路归并的头元素值,多出来的一位放MIN void adjust(int s,int *buf){//s是需要调整的buf的下标 int t=(s+LEN)/2;//得到s的在败者树上面的父节点 while(t>0){//不断与父节点对比,直到败者树的根节点 if(buf[s]>buf[ls[t]]){//如果当前节点s(胜者)大于父节点 ls[t]^=s;//交换ls[t]和s s^=ls[t];//s记录胜者 ls[t]^=s;//父节点记录败者 } t/=2;//得到败者树的上一个父节点,继续与当前胜者s比较 } ls[0]=s;//最终的胜者记录于ls[0] } void build(int *buf){ buf[LEN]=MIN;//最后一位放MIN for(int i=0;i<LEN+1;++i) ls[i]=LEN;//所有败者树初始化为MIN的下标 for(i=0;i<LEN;++i) adjust(i,buf);//依次调整即可完成初始化 } int main() { //初始buf int tmp[5]={18,21,16,11,19}; memcpy(buf,tmp,LEN*sizeof(int)); build(buf); cout<<buf[ls[0]]<<endl;//输出11 //取出11后,buf[3]=17 int tmp1[5]={18,21,16,17,19}; memcpy(buf,tmp1,LEN*sizeof(int)); adjust(3,buf); cout<<buf[ls[0]]<<endl;//输出16 return 0; }
用最小堆也可以,两者都可以在log2(n)时间内找出最小值,数据大时败者树稍微快一点:
标题:班门弄斧,发一篇自己写的算法分析文章(续)
发信站:水木社区(SatSep218:12:162006),站内
昨晚睡觉之前,还在想堆和败者树到底哪一个更优的题目。终极通过心算得出答案(感爱好的人可以算一下,并不难):假如将N个数的分布看作是完全随机分布,则通过求和可以计算出,当从N个数中提取出一个新的数m时,假如该数比之前找到的n个最大的数中的最小的x大,则会替换x进进“堆”或者“败者树”。
在这一次重新维护堆和败者树的时候,堆的期看比较次数是2logn-1,而败者树是logn。但是由于败者树内部节点存放的是叶子节点的下标,在进行比较的时候需要间接取值(先访问内部节点得到下标,在用此下标取到叶子节点中存放的数),所以比较时败者树访问内存次数是堆的两倍。
而在交换次数上(堆是父节点和子节点交换,败者树是往上走的优越者和内部节点存放的败者进行交换,每次交换需要三次操纵),堆的交换次数期看值是1/2logn-1/4,而败者树是1/2logn(这里全都忽略了logn的取整题目)。
我不能确定到底一次比较和一次内存访问到底哪一个更快,我猜想是内存访问更快,那样的话,败者树的比较性能会比堆更优。而在交换方面,到底那1/4的差会有多大影响,我也不好说了。
看来除非做实验,很难分出堆和败者树的高下了。