利用败者树进行外部排序时,要根据叶节点构造败者树。仔细理解了这个构造的过程。
I.
一个直接的思路就是类似我们手工构造一个败者树的过程。开始初始化所有败者树的节点值为-1。依次填含有叶节点的节点。A包含叶节点,比较A的左右孩子节点(左孩子可能不是叶节点,右孩子是叶节点),败者填入A。胜者与A的父节点B比较,如果B没有填值,则胜者填入B;如果B已经填值,用胜者值与B的值比较,败者值填入B,然后又获得新的胜者值。再用新的胜者值与B的父节点C比较。重复类似的比较直至胜者值被填入某个节点中。
一个问题:填完所有的子节点含有叶节点的节点后,败者树就初始化完?一个不严格的理解是:虽然败者树的节点数只是numLeaves,没有包含叶节点,我们可以假想这个树包含叶节点,并且值都是-1。填完后所有子节点包含叶节点的节点后,意味着所有叶节点都已经插入到败者树中,则败者树初始化完成。
/*
初始化一个败者树,胜者是Key值小的元素。
numLeaves:叶节点个数
KeyT leaves[numLeaves+1]:节点数组下标从1开始,保存每个叶节点的Key值
intloserTree[numLeaves]:只存储每个叶节点在数组leaves中的下标。loserTree[0]存储冠军,所以节点数组下标1开始。
*/
template <class KeyT>
void InitTree(KeyT * leaves,intnumLeaves,int *loserTree){
for(int i=0;i<numLeaves;i++){ //初始化每个树的节点值为-1
loserTree[i]=-1;
}
int rchildInx=-1,lchildInx=-1;
//依次填入每个节点,同时调整父节点,保证每填入一个节点后就是一个正确的败者树。
for(int i=numLeaves-1; i>=(numLeaves/2); --i){
//左孩子的值
if(2*i>=numLeaves && 2*i<2*num){
lchildInx = 2*i-numLeaves+1;
}else{
assert(2*i>=0 && 2*i<num);
lchildInx = loserTree[2*i];
}
//右孩子的值
rchildInx = 2*i+1;
//比较后得到败者和胜者。
int victorInx=-1;
if(leaves[rchildInx]>leaves[lchildInx]){
loserTree[i] = rchildInx;
victorInx = lchildInx;
}else{
loserTree[i] = lchildInx;
victorInx = rchildInx;
}
//寻找胜者的位置
int j=i/2;
while(j>=0){
if(loserTree[j] == -1){
loserTree[j] = victorInx;
break;
}else{
if(leaves[victorInx]>leaves[loserTree[j]){
int temp = loserTree[j];
loserTree[j] = victorInx;
victorInx = temp;
}
j=j/2;
}
}
}
}
II.
书上使用了另外更好的方法。把构成初始败者树的过程看成调整的过程。调整的过程是:当我们已经构造好一个败者树,取走胜者,然后在这个胜者对应的叶节点中填入一个值后,我们就要调整新的树,使其成为败者树。那么假设有k个叶节点保存在数组A[k]中。
如何构造初始树?
假设初始败者树是Ti,假设叶节点是X[h,h+1,...,h+k-1],其中X[g](h<=g<=j)是胜者,用A[1]替换X[g]并且调整后得到Ti1,Ti1的败者就不可以是A[1],否则插入A[2]时,A[1]就被替换,最后形成的败者树就不是我们需要的败者树。一个更一般的逻辑就是:假设A[1...m](m<k)已经被替换得到一个新的败者树,那么这个新的败者树的胜者一定不能是A[1...m]中的某个值,必须是Ti原先的叶节点。如何来构造这样的一个初始败者树Ti?如果我让Ti原先的叶节点都小于(或者都大于)A中所有的值,那么可以满足上述要求。选用一个特例来构造Ti:Ti所有的节点都是最小值,这个最小值是比较的关键字类型的最小值。如果选用A中最小的叶节点值来构造Ti,也应该是可以的,但是那样多了一步:找到A中最小的值。
为什么不构造一个初始树,然后逐个替换叶节点?
例如,假设初始败者树是Ti,假设叶节点是X[h,h+1,...h+k-1]。A[1…k]是我们要构建的败者树的叶节点。用A[1]替换X[h],然后调整;再用A[2]替换x[h+1],然后调整;依次类推,把Ti所有的叶节点替换为A中的值,从而得到我们需要的败者树。
如果替换任意一个节点,调整的过程要复杂。
假如叶节点N1是任意一个叶节点,用一个新的值替换N1原先的值。比较N1和N2,可以得到N3的值,同时得到一个胜者值,假设为Victor。节点N4的值就需要重新确定。用Victor与子树S1的胜者进行比较。因为N1不是原先败者树的胜者,所以原先N4保存的值就不一定是子树S1的胜者。S1的胜者可能是N4的父节点的值,或者是N4某个层次的祖父节点的值。但如果节点N1原先的值是原先败者树的胜者,那么N4的值就是子树S1的胜者。构造原先败者树时,N4的值是子树S1的胜者和子树S2的胜者比较后的败者,既然N1的值是原先败者树的胜者,子树S2的胜者就是N1的值,那么子树S1的胜者与子树S2的胜者(也就是N1的值)比较就是败者,这个败者被填入节点N4,所以N4的值就是子树S1的胜者。
/*
叶子数组leaves中第q个节点是原先的胜者,现在已经取得了一个新的值。调整新的树使其又成为败者树。
numLeaves:叶节点个数
KeyT leaves[num+1]:节点数组下标从1开始,保存每个叶节点的Key值
intloserTree[num]:只存储每个叶节点在数组leaves中的下标。loserTree[0]存储冠军,所以节点数组下标从1开始
q:叶子数组leaves中第q个节点是原先的胜者,现在已经取得了一个新的值。
*/
template <class KeyT>
void AdjustTree(KeyT * leaves,intnumLeaves,int *loserTree,int q){
int fInx = (numLeaves+q-1)/2;
while(fInx>=0){
if(fInx == 0){
loserTree[0] = q;
break;
}
if(leaves[q]>leaves[loserTree[fInx]){
int tmp = loserTree[fInx];
loserTree[fInx] = q;
q = tmp;
}
fInx = fInx/2;
}
}
/*
已知叶节点构造败者树。
numLeaves:叶节点个数
KeyT leaves[num+1]:节点数组下标从1开始,保存每个叶节点的Key值
intloserTree[num]:只存储每个叶节点在数组leaves中的下标。loserTree[0]存储冠军,所以节点数组下标从1开始
*/
template <class KeyT>
void InitTree(KeyT * leaves,intnumLeaves,int *loserTree){
leaves[0]=MINKEY;
for(int i=0; i<numLeaves;i++){
loserTree[i] = 0;
}
for(int i=numLeaves; i>0; i--){
AdjustTree<KeyT>(leaves,numLeaves,loserTree,i);
}
}