對於外部排序演算法來說,直接影響演算法效率的因素為讀寫外存的次數,即次數越多,演算法效率越低。若想提高演算法的效率,即減少演算法執行過程中讀寫外存的次數,可以增加 k–路平衡歸併中的 k 值。
經過計算得知,如果毫無限度地增加 k 值,雖然會減少讀寫外存資料的次數,但會增加內部歸併的時間,得不償失。
對於 10 個臨時檔案,當採用 2-路平衡歸併時,若每次從 2 個檔案中想得到乙個最小值時只需比較 1 次;而採用 5-路平衡歸併時,若每次從 5 個檔案中想得到乙個最小值就需要比較 4 次。以上僅僅是得到乙個最小值記錄,如要得到整個臨時檔案,其耗費的時間就會相差很大。
為避免在增加 k 值的過程中影響內部歸併的效率,在進行 k-路歸併時可使用敗者樹來實現,該方法在增加 k 值時不會影響其內部歸併的效率。
敗者樹實現內部歸併
敗者樹是樹形選擇排序的一種變形,本身是一棵完全二叉樹。
對於無序表建立的完全二叉樹如圖1所示,構建此樹的目的是選出無序表中的最小值。
圖 1 勝者樹
這棵樹與敗者樹正好相反,是一棵勝者樹。因為樹中每個非終端結點(除葉子結點之外的其它結點)中的值都表示的是左右孩子相比較後的較小值(誰最小即為勝者)。例如葉子結點 49 和 38 相對比,由於 38 更小,所以其雙親結點中的值保留的是勝者 38。然後用 38 去繼續同上層去比較,一直比較到樹的根結點。
而敗者樹恰好相反,其雙親結點儲存的是左右孩子比較之後的失敗者,而勝利者則繼續同其它的勝者去比較。
例如圖 1 中,葉子結點 49 和 38 比較,38 更小,所以 38 是勝利者,49 為失敗者,但由於是敗者樹,所以其雙親結點儲存的應該是 49;同樣,葉子結點 65 和 97 比較,其雙親結點中儲存的是 97 ,而 65 則用來同 38 進行比較,65 會儲存到 97 和 49 的雙親結點的位置,38 繼續做後續的勝者比較,依次類推。
勝者樹和敗者樹的區別就是:勝者樹中的非終端結點中儲存的是勝利的一方;而敗者樹中的非終端結點儲存的是失敗的一方。而在比較過程中,都是拿勝者去比較。
圖 2 敗者樹
如圖 2 所示為一棵 5-路歸併的敗者樹,其中 b0—b4 為樹的葉子結點,分別為 5 個歸併段中儲存的記錄的關鍵字。 ls 為一維陣列,表示的是非終端結點,其中儲存的數值表示第幾歸併段(例如 b0 為第 0 個歸併段)。ls[0] 中儲存的為最終的勝者,表示當前第 3 歸併段中的關鍵字最小。
當最終勝者判斷完成後,只需要更新葉子結點 b3 的值,即匯入關鍵字 15,然後讓該結點不斷同其雙親結點所表示的關鍵字進行比較,敗者留在雙親結點中,勝者繼續向上比較。
例如,葉子結點 15 先同其雙親結點 ls[4] 中表示的 b4 中的 12 進行比較,12 為勝利者,則 ls[4] 改為 15,然後 12 繼續同 ls[2] 中表示的 10 做比較,10 為勝者,然後 10 繼續同其雙親結點 ls[1] 表示的 b1(關鍵字 9)作比較,最終 9 為勝者。整個過程如下圖所示:
注意:為了防止在歸併過程中某個歸併段變為空,處理的辦法為:可以在每個歸併段最後附加乙個關鍵字為最大值的記錄。這樣當某一時刻選出的冠軍為最大值時,表明 5 個歸併段已全部歸併完成。(因為只要還有記錄,最終的勝者就不可能是附加的最大值)。
敗者樹:
敗者樹是勝者樹的一種變體。在敗者樹中,用父結點記錄其左右子結點進行比賽的敗者,而讓勝者參加下一輪的比賽。敗者樹的根結點記錄的是敗者(數值大的),需要加乙個結點來記錄整個比賽的勝利者。採用敗者樹可以簡化重構的過程。
/*** 實驗題目:
* 實現多路平衡歸併演算法
* 實驗目的:
* 領會外排序中多路平衡歸併的執行過程和演算法設計
* 實驗內容:
* 編寫程式,模擬利用敗者樹實現5路歸併演算法的過程以求解以下問題:
* 設有5個檔案中記錄關鍵字如下:
* f0: f1: f2: f3 f4
* 要求將其歸併為乙個有序段並輸出。假設這些輸入檔案資料存放在記憶體中,輸出
* 結果直接在螢幕上顯示。
*/#include
#define max_size 20 // 每個檔案中的最多記錄
#define k 5 // 5路平衡歸併
#define max_key 32767 // 最大關鍵字值∞
#define min_key -32768 // 最小關鍵字值-∞
typedef int info_type;
typedef int key_type;
typedef struct
rec_type; // 檔案中記錄的型別
typedef struct
file_type; // 模擬的檔案型別
typedef int loser_tree; // 敗者樹為loser_tree[k]
/*------------------------以下為全域性變數------------------------*/
rec_type b[k]; // b中存放各段中取出的當前記錄
file_type f[k]; // 存放檔案記錄的陣列
/*-----------------初始化存放檔案記錄的陣列f---------------------*/
static void initial(void)
/*--------------從f[i]檔案中讀乙個記錄到b[i]中---------------*/
static void input(int i, int &key)
static int cnt = 0;
/*--------------沿從葉子結點b[s]到根結點ls[0]=5的路徑調整敗者樹--------------*/
// ls為一維陣列,表示的是非終端結點,其儲存的數值表示第幾歸併段,例如b[0]為第0個歸併段
static void adjust(loser_tree ls[k], int s) // s=4,3,2,1,0
t = t / 2;
}ls[0] = s; // 根結點存放第幾個歸併段
printf("\tls[0] = %d\n", ls[0]);
}/*--------------建立敗者樹ls--------------*/
static void create_loser_tree(loser_tree ls[k])
/*--------------顯示敗者樹----------------*/
static void display(loser_tree ls[k])
else if(b[ls[i]].key == min_key)
else
}printf("\n");
}/*--------------輸出f[q]中的當前記錄--------------*/
static void output(int q)
/*--------------利用敗者樹ls進行k路歸併到輸出-----------------*/
static void k_merge(loser_tree ls[k])
create_loser_tree(ls); // 建立敗者樹ls,選得最小關鍵字為b[ls[0]].key
display(ls);
while(b[ls[0]].key != max_key)
}int main(void)
f1: f2: f3 f4\n");
initial();
k_merge(ls);
return 0;
}
7 7 3 多路平衡歸併與敗者樹
歸併趟數s logm r 向下取整 從而增加歸併路數m可以減少歸併趟數s,進而減少訪問外存的次數 i o次數 然而,當增加歸併路數m時,內部歸併時間將增加。做內部歸併時,在m個元素中選擇關鍵字最小的記錄需要比較m 1次。每趟歸併n個元素最多需要作 n 1 m 1 次比較,s趟歸併總共需要的比較次數為...
多路歸併排序的實現
為了更好地測試程式,首先利用以下方法生成了1000000個整數,每個整數均不相同 生成隨機排列的1000000個數字,每個數字都不相同 const int size 1000000 int num size void produce srand unsigned time null 隨機交換隨機的兩...
多路歸併排序
我們有如下乙個問題 對於若干個長度相同的序列,將其合併成乙個有序的序列。暴力的方法顯然是不可取的,這裡可以利用優先佇列來處理這個問題。首先從簡單的開始,對於2路歸併排序,設兩個序列為,將,排序,有 a1 a2 a3 an b1 b2 b3 bn 建立乙個優先佇列,佇列中首先存入元素 a1,0 b1,...