败者树实现外排序

还是一道百度题目:有20个数组,每个数组有500个元素,并且是有序排列好的,现在在这20*500个数中找出排名前500的数 。将会用到下面的败者树方法,败者树详细请看下面。

     败者树在外排序中用到,每加入一个数字时,调整树需要o(lgk),比较快。外排序过程主要分为两个阶段:(1)初始化各归并段写入硬盘,初识化的方法,可利用内排序方法还可以一种叫置换选择排序的方法(参考数据结构--李春葆)。


为什么需要败者树

     外排序过程考虑时间代价时,主要考虑访问磁盘的次数。那么基于两路归并排序的缺点在哪里呢?主要是访问磁盘的次数太多了?请看下图:


      假设初始化归并段有m个,则二路归并需要访问硬盘的次数为log2(m)。按照这个方法,那是不是我们只要增加k就可以减少次数呢?答案是肯定的。就是说是k路归并的话,访问硬盘次数就是logk(m)。但是这里边存在一个矛盾:如果增大k,归并的时候比较次数增加了。那我们只要找到一种可以增大k,然后比较次数又比较少的方法就行了,这就是多路归并---败者树。看下面推导:


这里边logk(m)表示读取次数等于(log2(m)/log2(k)),比较次数(n-1),如果采用多路归并树的话比较次数log2(k),恰好与分母约掉,这样归并的比较次数与k无关了。


败者树调整策略

   (1)输入每个归并段的第一个记录作为归并树的叶子节点。建立初始化归并树。

    (2)两两相比较,父亲节点存储了两个节点比较的败者(节点较大的值);胜利者(较小者)可以参与更高层的比赛。这样树的顶端就是当次比较的冠军(最小者)。

    (3)调整败者树,当我们把最小者输入到输出文件以后,需要从相遇的归并段取出一个记录补上去。补回来的时候,我们就需要调整归并树,我们只需要沿着当前节点的父亲节点一直比较到顶端。比较的规则是与父亲节点比较(父亲节点只是记录了一个败者索引,我们需要通过索引找到相应的值进行比较),比较小的(胜者)可以参与更高层的比较,即可以跟他爷爷比较,一直向上,直到根节点。比较大的(失败者)留在当前节点。


败者树编程(K路归并)

    在实现利用败者树编程的时候,我们把败者树的叶节点和非叶点分开定义:

    (1)叶节点存放在:b[k+1],其中b[0..k-1]存放记录,b[k]存放了一个比所有记录一个最小值,表示虚节点。

    (2)败者节点存放:ls[k],ls[1...k-1]存放各次比较的败者数组索引。ls[0]存放了最后的冠军。

注意:这里每个叶节点都是连到非叶节点上的,这个叶节点就是我们的父节点,那我们怎么算出连到那个非叶节点上呢:通过t = (index + K)/2,得到我们父节点的索引t,这样我们在调整树的时候只需要比较b[ls[t]],然后一直比较就行了。


(1)败者树创建

      首先,是创建归并树,程序开始将ls[0...k-1]=K,表示第K+1(虚设)个归并段的记录当前最小。然后,我们从k-1到0,每次加入一个记录进行一次调整,算法自顶向下,直到所有记录加进来,归并树也就建好了。

[cpp]  view plain ?
  1. #include <iostream>  
  2.   
  3. using namespace std;  
  4.   
  5. #define  K  5 //表示5路归并  
  6. #define MIN INT_MIN;  
  7.   
  8. int b[K+1] = {17,5,10,39,15};  
  9. int ls[K] = {0};//记录败者的序号  
  10.   
  11. void Adjust(int s)  
  12. {  
  13.     for(int t=(s+K)/2; t>0; t=t/2){//t=(s+k),得到与之相连ls数组的索引  
  14.         if(b[s] > b[ls[t]])//父亲节点  
  15.         {    
  16.             int temp = s; //s永远是指向这一轮比赛最小节点   
  17.             s = ls[t];    
  18.             ls[t]=temp;    
  19.         }    
  20.     }  
  21.     ls[0] = s;//将最小节点的索引存储在ls[0]  
  22. }  
  23.   
  24. void CreateLoser()  
  25. {  
  26.     b[K] = MIN;  
  27.     int i;  
  28.     for(i=0;i<K;i++)ls[i]=K;    
  29.     for(i=K-1;i>=0;i--)Adjust(i); //加入一个基点,要进行调整   
  30.   
  31.   
  32. }  
  33. int main()  
  34. {  
  35.     CreateLoser();  
  36.     system("pause");  
  37.     return 0;  
  38. }  
      图示一下创建树的过程:





上图中最后一副应该是加入b[0] = 17,而不是b[1]=5

(2)归并排序

读入数据,创建归并树,判断b[ls[0]]==MAX,等于表示所有记录都已输出。不等于,输出当前冠军,然后从相应归并段读入数据填上。注意,如果相应的归并段已经空了,则填上MAX。下面给出伪代码:

[cpp]  view plain ?
  1. void K_Merge()  
  2. {  
  3.     for(int i=0;i<K;i++){  
  4.         input(i);//输入到b[i]  
  5.     }  
  6.     CreateLoser();  
  7.     while(b[ls[0]]!=MAXKEY){//只要不是最大值  
  8.         q = ls[0];//得到冠军的索引  
  9.         output(b[q]);  
  10.         intput(b[q]);  
  11.         Adjust(q);  
  12.     }  
  13. }  

(3)整个代码(http://blog.csdn.net/tiantangrenjian/article/details/6838491

[cpp]  view plain ?
  1. #include <iostream>    
  2. using namespace std;    
  3.     
  4. #define LEN 10          //最大归并段长    
  5. #define MINKEY -1     //默认全为正数    
  6. #define MAXKEY 100    //最大值,当一个段全部输出后的赋值    
  7.     
  8. struct Array    
  9. {    
  10.     int arr[LEN];    
  11.     int num;    
  12.     int pos;    
  13. }*A;    
  14.     
  15.     int k,count;    
  16.     int *LoserTree,*External;    
  17.     
  18. void Adjust(int s)    
  19. {    
  20.     int t=(s+k)/2;    
  21.     int temp;    
  22.     while(t>0)    
  23.     {    
  24.         if(External[s] > External[LoserTree[t]])    
  25.         {    
  26.             temp = s;    
  27.             s = LoserTree[t];    
  28.             LoserTree[t]=temp;    
  29.         }    
  30.         t=t/2;    
  31.     }    
  32.     LoserTree[0]=s;    
  33. }    
  34.     
  35. void CreateLoserTree()    
  36. {    
  37.     External[k]=MINKEY;    
  38.     int i;    
  39.     for(i=0;i<k;i++)LoserTree[i]=k;    
  40.     for(i=k-1;i>=0;i--)Adjust(i);    
  41. }    
  42.     
  43. void K_Merge()    
  44. {    
  45.     int i,p;    
  46.     for(i=0;i<k;i++)    
  47.     {    
  48.         p = A[i].pos;    
  49.         External[i]=A[i].arr[p];    
  50.         //cout<<External[i]<<",";    
  51.         A[i].pos++;    
  52.     }    
  53.     CreateLoserTree();    
  54.     int NO = 0;    
  55.     while(NO<count)    
  56.     {    
  57.         p=LoserTree[0];    
  58.         cout<<External[p]<<",";    
  59.         NO++;    
  60.         if(A[p].pos>=A[p].num)External[p]=MAXKEY;    
  61.         else     
  62.         {    
  63.             External[p]=A[p].arr[A[p].pos];    
  64.             A[p].pos++;    
  65.         }    
  66.         Adjust(p);    
  67.     }    
  68.     cout<<endl;    
  69. }    
  70.     
  71. int main()    
  72. {    
  73.     freopen("in.txt","r",stdin);    
  74.     
  75.     int i,j;    
  76.     count=0;    
  77.     cin>>k;    
  78.     A=(Array *)malloc(sizeof(Array)*k);    
  79.     for(i=0;i<k;i++)    
  80.     {    
  81.         cin>>A[i].num;    
  82.         count=count+A[i].num;    
  83.         for(j=0;j<A[i].num;j++)    
  84.         {    
  85.             cin>>A[i].arr[j];    
  86.         }    
  87.         A[i].pos=0;    
  88.     }    
  89.     LoserTree=(int *)malloc(sizeof(int)*k);    
  90.     External=(int *)malloc(sizeof(int)*(k+1));    
  91.     
  92.     K_Merge();    
  93.     
  94.     return 0;    
  95. } <span style="font-size:18px"><strong> </strong></span>  

注意点

    归并路数k增大时,相应的需要增加输入缓冲区个数。如果可供应的内存不变,这将减少每个缓冲区的容量,使得内外存交换数据次数增大。所以k值过大时,虽然归并次数减少,但读写外存次数会增加。

      另外了,考虑比较次数最小,可构造哈夫曼树。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值