勝者樹和敗者樹都是完全二叉樹,是樹形選擇排序的一種變型。每個葉子結點相當於一個選手,每個中間結點相當於一場比賽,每一層相當於一輪比賽。
不同的是,勝者樹的中間結點記錄的是勝者的標號;而敗者樹的中間結點記錄的敗者的標號。
勝者樹與敗者樹可以在log(n)的時間內找到最值。任何一個葉子結點的值改變后,利用中間結點的信息,還是能夠快速地找到最值。在k路歸並排序中經常用到。
一、勝者樹
勝者樹的一個優點是,如果一個選手的值改變了,可以很容易地修改這棵勝者樹。只需要沿着從該結點到根結點的路徑修改這棵二叉樹,而不必改變其他比賽的結果。
Fig. 1
Fig.1是一個勝者樹的示例。規定數值小者勝。
- b3 PK b4,b3勝b4負,內部結點ls[4]的值為3;
- b3 PK b0,b3勝b0負,內部結點ls[2]的值為3;
- b1 PK b2,b1勝b2負,內部結點ls[3]的值為1;
- b3 PK b1,b3勝b1負,內部結點ls[1]的值為3。.
當Fig. 1中葉子結點b3的值變為11時,重構的勝者樹如Fig. 2所示。
- b3 PK b4,b3勝b4負,內部結點ls[4]的值為3;
- b3 PK b0,b0勝b3負,內部結點ls[2]的值為0;
- b1 PK b2,b1勝b2負,內部結點ls[3]的值為1;
- b0 PK b1,b1勝b0負,內部結點ls[1]的值為1。.
Fig. 2
用勝者樹對n個節點實現排序操作,構建勝者樹和構建堆比較相似,區別在於勝者樹只有葉子節點存放了數據,中間節點記錄的是葉子節點間的關系。
leaves[n+1]:共有n個葉子節點,存儲下標從1到n
successTree[n]:存儲中間節點,存儲下標從1到n-1
對successTree中的數據從n-1到1,按照優勝策略不斷調整內部節點的數值,最后得到一顆勝者樹
冠軍節點的下標存儲在successTree[1]里,實現排序操作時,將該葉子節點的值打印輸出,並用一個比葉子節點中所有值都大的值MAX替換,然后對樹進行調整。勝者樹的調整是從葉子節點到根節點的自下而上的調整,每次都比較雙親節點的左右孩子節點,並把優勝者的下標志存儲在雙親節點中。
1 #include <stdio.h> 2 #define K 10 3 #define MAX 65535 4 int leaves[K+1]; 5 int successTree[K]; 6 7 /* 對於單個內部節點進行調整 */ 8 void adjust(int i) 9 { 10 int m,n; 11 if(2 * i < K) /* 獲取它的左孩子結點 */ 12 m = successTree[2 * i]; 13 else 14 m = 2 * i - K + 1; 15 if(2*i+1<K) /* 獲取它的右孩子節點 */ 16 n = successTree[2*i+1]; 17 else 18 n = 2 * i + - K + 2; 19 successTree[i] = leaves[m] > leaves[n] ? n : m; /* 進行勝負判定 */ 20 } 21 /* 初始化葉子節點並對內部節點進行類似於堆的調整 */ 22 void initTree() 23 { 24 for(int i=1;i<K+1;i++) 25 scanf("%d", &leaves[i]); 26 for(int i=K-1;i>0;i--) 27 adjust(i); 28 } 29 /* 自下而上對勝者樹進行調整 */ 30 void adjustToRoot(int i) 31 { 32 int parent = (i + K - 1) / 2; /* 對從當前節點到根節點路徑上的所有 33 * 節點進行調整 */ 34 while(parent>0) 35 { 36 adjust(parent); 37 parent = parent / 2; 38 } 39 } 40 41 int main() 42 { 43 freopen("in","r",stdin); 44 initTree(); 45 for(int i=1;i<K+1;i++) /* 每次用最大值替換掉冠軍節點,並對樹 46 * 進行調整,最終得到升序排序的序列 */ 47 { 48 printf("%d ", leaves[successTree[1]]); 49 leaves[successTree[1]]=MAX; 50 adjustToRoot(successTree[1]); 51 } 52 return 0; 53 }
二、敗者樹
敗者樹是勝者樹的一種變體。在敗者樹中,用父結點記錄其左右子結點進行比賽的敗者,而讓勝者參加下一輪的比賽。敗者樹的根結點記錄的是敗者,需要加一個結點來記錄整個比賽的勝利者。采用敗者樹可以簡化重構的過程。
Fig. 3
Fig. 3是一棵敗者樹。規定數大者敗。
- b3 PK b4,b3勝b4負,內部結點ls[4]的值為4;
- b3 PK b0,b3勝b0負,內部結點ls[2]的值為0;
- b1 PK b2,b1勝b2負,內部結點ls[3]的值為2;
- b3 PK b1,b3勝b1負,內部結點ls[1]的值為1;
- 在根結點ls[1]上又加了一個結點ls[0]=3,記錄的最后的勝者。
敗者樹重構過程如下:
- 將新進入選擇樹的結點與其父結點進行比賽:將敗者存放在父結點中;而勝者再與上一級的父結點比較。
- 比賽沿着到根結點的路徑不斷進行,直到ls[1]處。把敗者存放在結點ls[1]中,勝者存放在ls[0]中。
Fig. 4
Fig. 4是當b3變為13時,敗者樹的重構圖。
注意,敗者樹的重構跟勝者樹是不一樣的,敗者樹的重構只需要與其父結點比較,而勝者樹則需要和兄弟節點比較。對照Fig. 3來看,b3與結點ls[4]的原值比較,ls[4]中存放的原值是結點4,即b3與b4比較,b3負b4勝,則修改ls[4]的值為結點3。同理,以此類推,沿着根結點不斷比賽,直至結束。
敗者樹常常用於多路外部排序,對於K個已經排好序的文件,將其歸並為一個有序文件。敗者樹的葉子節點是數據節點,兩兩分組,內部節點記錄左右子樹中的“敗者”,優勝者往上傳遞一直到根節點,如果規定優勝者是兩個數中的較小者,則根節點記錄的是最后一次比較中的敗者,也就是第二小的數,而用一個變量來記錄最小的數。把最小值輸出以后,用一個新的值替換最小值節點的值(在文件歸並的時候,如果文件已經讀完,可以用一個無窮大的數來替換),接下來維護敗者樹,從更新的節點往上,一次與父節點比較,將敗者更新,勝者繼續比較。
注意:當葉子節點的個數變動的時候需要完全重新構建整棵樹。
比較敗者樹和堆的性能
敗者樹在維護的時候,比較次數是logn+1, 敗者樹從下往上維護,每上一層,只需要和父節點比較一次,而堆是自上往下維護,每一層需要和左右子節點都比較,需要比較兩次,從這個角度,敗者樹比堆更優一點,但是,敗者樹每一次維護,必然是從葉子節點到根節點的一條路徑,而堆維護的時候有可能在中間某個層次停止,這樣敗者樹雖然每層比堆比較的次數少,但是堆比較的層數可能比較少。
從n個數中找出最大的k個,分別用堆和敗者樹來實現
堆實現: 維護一個大小為k的小頂堆,每來一個數都和堆頂進行比較,如果比堆頂小,直接舍棄,否則替換堆頂,維護堆,直到n個數都處理完畢,時間復雜度為O(nlogk)
敗者樹實現:當用數組來實現敗者樹時, 維護一個葉子節點個數為k的敗者樹,注意是葉子節點個數而不是所有節點個數,數字較小者取勝,則最頂層保存的是值最小的葉子節點,每來一個數和最小值比較,如果比最小值還小,直接舍棄,否則替換最小值的節點值,從下往上維護敗者樹,最后的k個葉子節點中保存的就是所有數中值最大的k的,時間復雜度為O(nlogk)
用數組實現敗者樹的時候,因為只有葉子節點存儲的是數據,因此敗者樹使用的內存空間是堆的兩倍。
完全樹的內部,度數為2的節點個數是葉子節點個數減一 ,所以使用的數組大小為2k-1, 如果把最值也存入數組中,則需要的數組大小為2k
敗者樹的構造
思路: 先構造一顆空的敗者樹,然后把葉子節點一個一個的插入敗者樹,自底向上不斷的調整,保持內部節點保存的都是失敗者的節點編號,優勝者一直向上不斷比較,最終得到一顆合格的敗者樹。
leaves[K+1] : 葉子節點的個數為K,下標從1到K,下標0處存儲一個最小值,用來初始化敗者樹
loserTree[K]: 冠軍節點存儲在下標0,下標1到K-1存儲內部節點
int loserTree[K]; /* 存儲中間節點值,下標0處存儲冠軍節點 */ int leaves[K+1]; /* 從下標1開始存儲葉子節點值,下標0處存儲一個最小值節點 */ void adjust(int i) { int parent=(i+K-1)/2; /* 求出父節點的下標 */ while(parent>0) { if(leaves[i]>leaves[loserTree[parent]]) { int temp=loserTree[parent]; loserTree[parent]=i; /* i指向的是優勝者 */ i= temp; } parent = parent / 2; } loserTree[0]=i; } void initLoserTree() { int i; for(i=1;i<K+1;i++) scanf("%d",&leaves[i]); leaves[0]=MIN; for(int i=0;i<K;i++) loserTree[i]=0; for(int i=K;i>0;i--) adjust(i); }