一:胜者树(http://www.haogongju.net/art/2266943)
问题描述:给出一个长度是N的数组,现在要找出最小的两个元素,最少要多少次比较。
分析: 如果找出1个最小的,比较次数无疑是n-1;如果用选择排序,再取选择第二个最小的又得比较n-2次。这种寻找的办法其实是可以优化的,在第一次寻找最小元素过程中,其实我们已经比较了很多元素了,那么为什么不利用前面比较的结果来寻找第二个最小的呢。
这用到胜者树的数据结构,这样就可以再logn就可以找到了第二小的元素。胜者树在求数组最大值,次大值得时候,有用武之地。
胜者树排序,属于选择排序的一种。直接选择排序之所以不够高效就是因为没有把前一趟比较的结果保留下来,每次都有很多重复的比较。胜者树排序就是要克服这一缺点。它的基本思想与体育淘汰赛类似,首先取得n个元素的关键字,进行两两比较,得到 n/2 个比较的优胜者,将其作为第一次比较的结果保留下来,然后对这些元素再进行关键值的两两比较,…,如此重复,直到选出一个关键字最小的对象为止。
下面举个例子,假设arr[] = {3,4,1,6,2,8,7,9},我们首先需要建立一棵完全二叉树,数组arr的元素分布在叶子节点上,内部节点存储了比赛的结果。如下图所示的完全二叉树:
完全二叉树的性质就是:内部节点数=叶子节点数-1。所以,该胜者树的数据结构可以这样存储:
根据上面的示意图,其实我们还需要一个变量来存储胜者的索引。我们再来画一下图,逗号后面记录了胜者的索引:
于是,第一次建树,就成上面这样了,我们可以轻松通过根节点得到最后的胜者(最小节点),并且同时知道胜者的索引号。下次在搜索最小值的时候,我们需要将刚才胜者值替换为最大值,然后沿着红色的线比较一遍就行了,这次只需要比较三次就行了(logn),请看下图。
然后,一直进行这个过程中就可以完成对数组的排序,排序的时间复杂度。
从这个演示可以看出这个算法真正吸引我们的地方就是当决出一个胜者后,要取得下一个胜者的比较只限于从根到刚才选出的外结点这一条路径上。可以看出除第一次比较需要n-1次外,此后选出次小,再次小......的比较都是log n次,故其复杂度为O(nlogn)。但是对于有n个待排元素,锦标赛算法需要至少2n-1个结点来存放胜者树。故这是一个拿空间换时间的算法。
代码如下:
typedef struct
{
int index;
int data;
}TreeNode;
TreeNode *buildWinnerTree(int *set, int setsize)
{
int treesize = 2 * setsize - 1;
TreeNode *tree = malloc(treesize *sizeof(TreeNode));
assert(tree);
int i;
for(i = 0; i < setsize-1; i++)
{
tree[i].data = MAX;
}
for(i = setsize-1; i < treesize;i++)
{
tree[i].data= set[i - setsize + 1];
tree[i].index= i;
}
int p,l,r;
p =(treesize-2)/2;
while(p >= 0)
{
l = 2 * p + 1;
r = 2 * p + 2;
if(tree[l].data >tree[r].data)
{
tree[p] = tree[r];
}
else
{
tree[p] = tree[l];
}
p--;
}
return tree;
}
void printkth(TreeNode *tree, int kth)
{
int index;
int i;
for(i = 0; i < kth; i++)
{
printf("the %dth is%d\n", i, tree[0].data);
index = tree[0].index;
tree[index].data = MAX;
adjustTree(tree, index);
}
}
void adjustTree(TreeNode *tree, int index)
{
int p,l,r;
p = (index-1) / 2;
while(p >= 0)
{
l = 2 * p + 1;
r = 2 * p + 2;
if(tree[l].data >tree[r].data)
{
tree[p] = tree[r];
}
else
{
tree[p] = tree[l];
}
if(p > 0)p = (p-1)/2;
else break;
}
}
二:败者树
败者树也是一个完全二叉树,一颗败者树中的结点有叶子节点和内部结点。其中叶子结点就是原数组,即需要抽取最小值的数组。内部结点代表失败者。在实现败者树的时候,分别用两个数组表示:ls数组表示内部结点,从0到k-1.b数组表示叶子节点,也就是原数组,从0到k-1.
败者树中用父结点记录其左右子结点进行比赛的败者,而让胜者参加下一轮的比赛。败者树的根结点记录的是败者,需要加一个结点来记录整个比赛的胜利者。采用败者树可以简化重构的过程。如下图:
在上图中,方框表示叶子结点,圆圈表示内部结点。可以看见内部结点为ls[0..4],叶子节点为b[0..4]。上图的败者树,构造过程如下(小者为胜):
a:b3 Vs b4,b3胜b4负,内部结点ls[4]的值为4,表示b4为败者;胜者b3继续参与竞争。
b:b3 Vsb0,b3胜b0负,内部结点ls[2]的值为0,表示b0为败者;胜者b3继续参与竞争。
c:b1 Vs b2,b1胜b2负,内部结点ls[3]的值为2,表示b2为败者;胜者b1继续参与竞争。
d:b3 Vs b1,b3胜b1负,内部结点ls[1]的值为1,表示b1为败者;胜者b3为最终冠军,用ls[0]=3,记录的最后的胜者索引。
败者树重构过程如下:
将新进入选择树的结点与其父结点进行比赛:将败者存放在父结点中;而胜者再与上一级的父结点比较。比赛沿着到根结点的路径不断进行,直到ls[1]处。把败者存放在结点ls[1]中,胜者存放在ls[0]中。如下图:
当b3变为13时,败者树的重构过程如下:
a:b3 Vs b[ls[4]],也就是b3 Vsb4,b4胜,继续参加下面的竞争,ls[4]=3记录败者。
b:b4Vs b[ls[2]],也就是b4 Vs b0, b0胜,继续参加下面的竞争,ls[2]=4记录败者。
c:b0Vs b[ls[1]],也就是b0 Vs b1, b1胜,b1为最终冠军,所以ls[0]=1记录冠军,ls[1]=0记录败者。
代码如下:
void Adjust(int s)
{
int t=(s+k)/2; //t为内部结点,也就是s的父节点
while(t>0)
{
if(b[s]>b[ls[t]])
{
int tmp=s;
s=ls[t]; //s记录新的胜者
ls[t]=tmp;
}
t=t/2;
}
ls[0]=s;
}
void CreateLoserTree()
{
int i;
b[k]=MIN;
for(i=0;i
for(i=k-1;i>=0;i--)
Adjust(i);
}
败者树简化了重构。败者树的重构只是与该结点的父结点的记录有关,而胜者树的重构还与该结点的兄弟结点有关。所以败者树在外排序的k路平衡归并中使用。利用败者树进行外部排序的代码如下:
void K_Merge()
{
FILE *fout = fopen(m_out_file,"wt");
FILE* *farray = new FILE*[k];
int i;
for(i = 0; i < k; ++i)
{
char* fileName =temp_filename(i);
farray[i] = fopen(fileName,"rt");
free(fileName);
}
for(i = 0; i < k; ++i)
{
if(fscanf(farray[i],"%d", &b[i]) == EOF)
{
b[i] = MAX;
}
}
CreateLoserTree();
int q;
while(b[ls[0]]!=MAX)
{
q=ls[0];
fprintf(fout,"%d ",b[q]);
if(fscanf(farray[q],"%d",&b[q])== EOF)b[q] = MAX;
Adjust(q);
}
for(i = 0; i < k; ++i)
{
fclose(farray[i]);
}
delete [] farray;
fclose(fout);
}
http://www.haodaima.net/art/2266943