外排序
第九章 排序 |
||
9.7 外排序
當待排序的對象數目特別多時,在內存中不能一次處理。必須把它們以文件的形式存放於外存,排序時再把它們一部分一部分調入內存進行處理。這樣,在排序過程中必須不斷地在內存與外存之間傳送數據。這種基於外部存儲設備(或文件)的排序技術就是外排序。
9.7.1 外排序的基本過程
n 當對象以文件形式存放於磁盤上的時候,通常是按物理塊存儲的。 n 物理塊也叫做頁塊,是磁盤存取的基本單位。
n 每個頁塊可以存放幾個對象。操作系統按頁塊對磁盤上的信息進行讀寫。 n 本節所指的磁盤是由若干片磁盤組成的磁盤組,各個盤片安裝在同一主軸上高速旋轉。各個盤面上半徑相同的磁道構成了柱面。各盤面設置一個讀寫磁頭,它們裝在同一動臂上,可以徑向從一個柱面移到另一個柱面上。 n 爲了訪問某一頁塊,先尋找柱面,移動臂使讀寫磁頭移到指定柱面上:尋查(seek)。 再根據磁道號(盤面號)選擇相應讀寫磁頭,等待指定頁塊轉到讀寫磁頭下:等待(latency)。因此, 在磁盤組上存取一個頁塊的時間:
tio=tseek+tlatency+trw
n基於磁盤進行的排序多使用歸併排序方法。其排序過程主要分爲兩個階段: u 第一個階段建立用於外排序的內存緩衝區。根據它們的大小將輸入文件劃分爲若干段,用某種內排序方法對各段進行排序。這些經過排序的段叫做初始歸併段或初始順串 (Run)。當它們生成後就被寫到外存中去。 u 第二個階段仿照內排序中所介紹過的歸併樹模式,把第一階段生成的初始歸併段加以歸併,一趟趟地擴大歸併段和減少歸併段個數,直到最後歸併成一個大歸併段(有序文件)爲止。
n示例:設有一個包含4500個對象的輸入文件。現用一臺其內存至多可容納750個對象的計算機對該文件進行排序。輸入文件放在磁盤上,磁盤每個頁塊可容納250個對象,這樣全部對象可存儲在 4500 / 250=18 個頁塊中。輸出文件也放在磁盤上,用以存放歸併結果。 n 由於內存中可用於排序的存儲區域能容納750 個對象, 因此內存中恰好能存3個頁塊的對象。 n 在外歸併排序一開始,把18塊對象,每3塊一組,讀入內存。利用某種內排序方法進行內排序, 形成初始歸併段, 再寫回外存。總共可得到6個初始歸併段。然後一趟一趟進行歸併排序。
n 若把內存區域等份地分爲 3 個緩衝區。其中的兩個爲輸入緩衝區,一個爲輸出緩衝區,可以在內存中利用簡單 2 路歸併函數 merge 實現 2 路歸併。 n首先, 從參加歸併排序的兩個輸入歸併段 R1 和 R2 中分別讀入一塊,放在輸入緩衝區1 和輸入緩衝區2 中。然後,在內存中進行2路歸併,歸併出來的對象順序存放到輸出緩衝區中。
n 一般地,若總對象個數爲 n,磁盤上每個頁塊可容納 b 個對象,內存緩衝區可容納 i 個頁塊,則每個初始歸併段長度爲 len = i * b,可生成 m = én / lenù 個等長的初始歸併段。 n 在做2路歸併排序時,第一趟從 m 個初始歸併段得到 ém/2ù 個歸併段,以後各趟將從 l (l >1) 個歸併段得到 él/2ù 個歸併段。總歸併趟數等於歸併樹的高度 élog2mù。 n 根據 2 路歸併樹, 估計 2 路歸併排序時間 tES 的上界爲:
tES = m*tIS + d*tIO + S*u*tmg
n對4500個對象進行排序的例子,各種操作的計算時間如下: u 讀18個輸入塊, 內部排序6段, 寫18個輸出塊 =6 tIS+36 tIO u 成對歸併初始歸併段 R1~R6 =36 tIO+4500 tmg u 歸併兩個具有1500個對象的歸併段R12和R34 =24 tIO+3000 tmg u 最後將 R1234 和 R56 歸併成一個歸併段 = 36 tIO+4500 tmg n 合計 tES=6 tIS+132 tIO+12000 tmg
n 由於 tIO = tseek + tlatency +trw, 其中,tseek和tlatency是機械動作,而trw、tIS、tmg是電子線路的動作,所以 tIO >> tIS,tIO >> tmg。想要提高外排序的速度,應着眼於減少 d。 n 若對相同數目的對象,在同樣頁塊大小的情況下做 3 路歸併或做 6 路歸併(當然, 內存緩衝區的數目也要變化),則可做大致比較:
歸併路數 k 總讀寫磁盤次數 d 歸併趟數 S 2 132 3 3 108 2 6 72 1
n因此,增大歸併路數,可減少歸併趟數,從而減少總讀寫磁盤次數d。
n 一般, 對 m 個初始歸併段, 做 k 路平衡歸併, 歸併樹可用正則 k 叉樹(即只有度爲 k 與度爲0的結點的 k 叉樹)來表示。 n 第一趟可將 m 個初始歸併段歸併爲 l = ém/kù 個歸併段,以後每一趟歸併將 l 個歸併段歸併成 l = él / kù 個歸併段,直到最後形成一個大的歸併段爲止。樹的高度= élogkmù = 歸併趟數S。 n 只要增大歸併路數 k,或減少初始歸併段個數 m,都能減少歸併趟數 S,以減少讀寫磁盤次數 d,達到提高外排序速度的目的。 n採用輸入緩衝區、內部歸併和輸出緩衝區並行處理的方法,也能有效地提高外排序的速度。
9.7.2 k路平衡歸併 (k-way Balanced merging)
n 做 k 路平衡歸併時,如果有 m 個初始歸併段,則相應的歸併樹有 élogkmù +1 層,需要歸併élogkmù 趟。下圖給出對有36個初始歸併段的文件做6路平衡歸併時的歸併樹。
n 做內部 k 路歸併時,在 k 個對象中選擇最小者,需要順序比較 k-1 次。每趟歸併 u 個對象需要做(u-1)*(k-1)次比較,S 趟歸併總共需要的比較次數爲: S*(u-1)*(k-1) = élogkmù * (u-1) * (k-1) = élog2mù * (u-1) * (k-1) / élog2kù n 在初始歸併段個數 m 與對象個數 u 一定時, élog2mù*(u-1) = const,而 (k-1) / élog2kù 在 k 增大時趨於無窮大。因此,增大歸併路數 k,會使得內部歸併的時間增大。 n 使用“敗者樹”從 k 個歸併段中選最小者,當 k 較大時 (k >= 6),選出關鍵碼最小的對象只需比較 élog2kù 次。
S*(u-1)*élog2kù = élogkmù * (u-1) * élog2kù = élog2mù * (u-1) * élog2kù / élog2kù = élog2mù * (u-1) n 關鍵碼比較次數與 k 無關,總的內部歸併時間不會隨 k 的增大而增大。 n因此,只要內存空間允許, 增大歸併路數 k, 將有效地減少歸併樹深度, 從而減少讀寫磁盤次數 d, 提高外排序的速度。 n 下面討論利用敗者樹在 k 個輸入歸併段中選擇最小者,實現歸併排序的方法。
n敗者樹是一棵正則的完全二叉樹。其中 u 每個葉結點存放各歸併段在歸併過程中當前參加比較的對象; u 每個非葉結點記憶它兩個子女結點中對象關鍵碼小的結點(即敗者); n 因此,根結點中記憶樹中當前對象關鍵碼最小的結點 (最小對象)。 n 敗者樹與勝者樹的區別在於一個選擇了敗者(關鍵碼大者),一個選擇了勝者(關鍵碼小者)。
n示例:設有5個初始歸併段,它們中各對象的關鍵碼分別是:
n 敗者樹的高度爲 [log2],在每次調整,找下一 個具有最小關鍵碼對象時, 最多做 [log2k]次關鍵碼比較。 n 在內存中應爲每一個歸併段分配一個輸入緩衝區,其大小應能容納一個頁塊的對象,編號與歸併段號一致。每個輸入緩衝區應有一個指針,指示當前參加歸併的對象。 n在內存中還應設立一個輸出緩衝區,其大小相當於一個頁塊大小。它也有一個緩衝區指針,指示當前可存放結果對象的位置。每當一個對象 i 被選出,就執行OutputRecord(i)操作,將對象按輸出緩衝區指針所指位置存放到輸出緩衝區中。
n 在實現利用敗者樹進行多路平衡歸併算法時,把敗者樹的葉結點和非葉結點分開定義。 n 敗者樹葉結點key[k]有k+1個,key[0]到key[k-1]存放各歸併段當前參加歸併的對象的關鍵碼,key[k]是輔助工作單元,在初始建立敗者樹時使用,存放一個最小的在各歸併段中不可能出現的關鍵碼:-MaxNum。 n 敗者樹非葉結點loser[k-1]有 k 個,其中loser[1]到loser[k-1]存放各次比較的敗者的歸併段號,loser[0]中是最後勝者所在的歸併段號。另外還有一個對象數組 r[k],存放各歸併段當前參加歸併的對象。
k 路平衡歸併排序算法 void kwaymerge ( Element *r ) { r = new Element[k]; //創建對象數組 int *key = new int[k+1]; //創建外結點數組 int *loser = new int[k]; //創建敗者樹數組 for ( int i = 0; i < k; i++ ) //傳送參選關鍵碼 { InputRecord ( r[i] ); key[i] = r[i].key; } for ( i = 0; i < k; i++) loser[i] = k; key[k] = -MaxNum; //初始化 for ( i = k-1; i; i-- ) //調整形成敗者樹 adjust ( key, loser, k, i );
while ( key[loser[0]] != MaxNum ) { //選歸併段 q = loser[0]; //最小對象的段號 OutputRecord ( r[q] ); //輸出 InputRecord ( r[q] ); //從該段補入對象 key[q] = r[q].key; adjust ( key, loser, k, q ); //調整 } Output end of run marker; //輸出段結束標誌 delete [ ] r; delete [ ] key; delete [ ] loser; }
自某葉結點key[q]到敗者樹根結點的調整算法 void adjust ( int key[ ]; int loser[ ]; const int k; const int q ) { //q指示敗者樹的某外結點key[q], 從該結點起到根 //結點進行比較, 將最小 key 對象所在歸併段的段 //號記入loser[0]。k是外結點key[0..k-1]的個數。 for ( int t = (k+q) / 2; t > 0; t /= 2 ) // t是q的雙親 if ( key[loser[t]] < key[q]) { //敗者記入loser[t], 勝者記入q int temp = q; q = loser[t]; loser[t] = temp; } //q與loser[t]交換 loser[0] = q; }
n 以後每選出一個當前關鍵碼最小的對象,就需要在將它送入輸出緩衝區之後,從相應歸併段的輸入緩衝區中取出下一個參加歸併的對象,替換已經取走的最小對象,再從葉結點到根結點,沿某一特定路徑進行調整,將下一個關鍵碼最小對象的歸併段號調整到loser[0]中。 n 最後,段結束標誌MaxNum升入loser[0],排序完成,輸出一個段結束標誌。 n歸併路數 k 的選擇不是越大越好。歸併路數 k增大時,相應需增加輸入緩衝區個數。如果可供使用的內存空間不變,勢必要減少每個輸入緩衝區的容量,使內外存交換數據的次數增大。
9.7.3初始歸併段的生成 (Run Generation)
爲了減少讀寫磁盤次數,除增加歸併路數 k 外,還可減少初始歸併段個數 m。在總對象數n 一定時,要減少 m,必須增大初始歸併段長度。 如果規定每個初始歸併段等長,則此長度應根據生成它的內存工作區空間大小而定,因而m的減少也就受到了限制。 爲了突破這個限制,可採用敗者樹來生成初始歸併段。在使用同樣大的內存工作區的情況下,可以生成平均比原來等長情況下大一倍的初始歸併段,從而減少參加多路平衡歸併排序的初始歸併段個數,降低歸併趟數。 圖解舉例說明如何利用敗者樹產生較長的初始歸併段。設輸入文件FI中各對象的關鍵碼序列爲 { 17, 21, 05, 44, 10, 12, 56, 32, 29 }。 選擇和置換過程的步驟如下:
1、從輸入文件FI中把 k 個對象讀入內存中,並構造敗者樹。(內存中存放對象的數組r可容納的對象個數爲 k ) 2、利用敗者樹在 r 中選擇一個關鍵碼最小的對象r[q],其關鍵碼存入LastKey作爲門檻 。以後再選出的關鍵碼比它大的對象歸入本歸併段,比它小的歸入下一歸併段。 3、將此r[q]對象寫到輸出文件FO中。(q是葉結點序號) 4、 若FI未讀完, 則從FI讀入下一個對象, 置換r[q]及敗者樹中的key[q]。 5、 調整敗者樹,從所有關鍵碼比LastKey大的對象中選擇一個關鍵碼最小的對象r[q]作爲門檻,其關鍵碼存入LastKey。 6、重複3~5, 直到在敗者樹中選不出關鍵碼比LastKey大的對象爲止。此時, 在輸出文件FO中得到一個初始歸併段, 在它最後加一個歸併段結束標誌。 7、重複2~6, 重新開始選擇和置換,產生新的初始歸併段,直到輸入文件FI中所有對象選完爲止。
n 若按在 k 路平衡歸併排序中所講的,每個初始歸併段的長度與內存工作區的長度一致, 則上述9個對象可分成3個初始歸併段: Run0 { 05, 17, 21 } Run1 { 10, 12, 44 } Run2 { 29, 32, 56 } n 但採用上述選擇與置換的方法, 可生成2個長度不等的初始歸併段: Run0 { 05, 17, 21, 44, 56 } Run1 { 10, 12, 29, 32 } n
n 在利用敗者樹生成不等長初始歸併段的算法和調整敗者樹並選出最小對象的算法中,用兩個條件來決定誰爲敗者,誰爲勝者。
u 首先比較兩個對象所在歸併段的段號,段號小者爲勝者,段號大者爲敗者; u 在歸併段的段號相同時,關鍵碼小者爲勝者,關鍵碼大者爲敗者。 n 比較後把敗者對象在對象數組 r 中的序號記入它的雙親結點中,把勝者對象在對象數組 r 中的序號記入工作單元 s 中,向更上一層進行比較,最後的勝者記入 loser[0]中。
利用敗者樹生成初始歸併段
void generateRuns ( Element *r ) { r = new Element[k]; int *key = new int[k]; //參選對象關鍵碼數組 int *rn = new int[k]; //參選對象段號數組 int *loser = new int[k]; //敗者樹 for ( int i = 0; i < k; i++ ){ loser[i] = 0; rn[i] = 0;} for ( i = k-1; i > 0; i-- ) { //輸入首批對象 if ( end of input ) rn[i] = 2; //中途結束 else { InputRecord ( r[i] ); //從緩衝區輸入 key[i] = r[i].key; rn[i] = 1; }
SelectMin ( key, rn, loser, k, i, rq ); //調整 } q = loser[0]; // q是最小對象在 r 中的序號 int rq = 1; // r[q]的歸併段段號 int rc = 1; //當前歸併段段號 int rmax = 1; //下次將要產生的歸併段段號 int LastKey = MaxNum; //門檻 while (1) { //生成一個初始歸併段 if ( rq != rc ) { //當前最小對象歸併段大 Output end of run marker; //加段結束符 if ( rq > rmax ) return; //處理結束 else rc = rq; //否則置當前段號等於rq }
OutputRecord ( r[q] ); // rc==rq,輸出 LastKey = Key[q]; //置新的門檻 if ( end of input ) rn[q] = rmax + 1; //虛設對象 else { //輸入文件未讀完 InputRecord ( r[q] ); //讀入到剛纔位置 key[i] = r[i].key; if ( key[q] < LastKey ) //小於門檻 rn[q] = rmax = rq + 1; //應爲下一段對象 else rn[q] = rc; //否則在當前段 } rq = rn[q]; SelectMin ( key, rn, loser, k, q, rq ); q = loser[0]; //新的最小對象
} // end of while delete [ ] r; delete [ ] key; delete [ ] rn; delete [ ] loser; }
在敗者樹中選擇最小對象的算法
void SelectMin ( int key[ ]; int rn[ ]; int loser[ ]; const int k; const int q; int &rq ) { //q指示敗者樹的某外結點key[q], 從該結點向上到//根結點loser[0]進行比較, 選擇出LastKey對象。k //是外結點key[0..k-1]的個數。
for ( int t = (k+q)/2; t > 0; t /= 2 ) if ( rn[loser[t]] < rq || rn[loser[t]] == rq && key[loser[t]] < key[q] ) { //先比較段號再比較關鍵碼, 小者爲勝者 int temp = q; q = loser[t]; loser[t] = temp; //敗者記入loser[t], 勝者記入q rq = rn[q]; } loser[0] = q; //最後的勝者 }
9.7.4 並行操作的緩衝區處理
n 如果採用 k 路歸併對 k 個歸併段進行歸併,至少需要 k 個輸入緩衝區和 1 個輸出緩衝區。每個緩衝區存放一個頁塊的信息。 n 但要同時進行輸入、內部歸併、輸出操作,這些緩衝區就不夠了。例如, u
n 由於內外存信息傳輸的時間與CPU的運行時間相比要長得多,所以使得內部歸併經常處於等待狀態。 n 爲了改變這種狀態,希望使輸入、內部歸併、輸出並行進行。對於 k 路歸併,必須設置 2k 個輸入緩衝區和 2 個輸出緩衝區。 n示例:給每一個歸併段固定分配 2 個輸入緩衝區,做 2 路歸併。假設存在 2 個歸併段: u Run0:對象的關鍵碼是 1, 3, 7, 8, 9 u Run1:對象的關鍵碼是 2, 4, 15, 20, 25 n 假設每個緩衝區可容納 2 個對象。需要設置 4 個輸入緩衝區IB[i], 1£ i £ 4,2 個輸出緩衝區OB[0]和OB[1]。 n 因此,不應爲各歸併段分別分配固定的兩個緩衝區,緩衝區的分配應當是動態的,可根據需要爲某一歸併段分配緩衝區。但不論何時,每個歸併段至少需要一個包含來自該歸併段的對象的輸入緩衝區。
k 路歸併時動態分配緩衝區的實施步驟
¶ 1、爲 k 個初始歸併段各建立一個緩衝區的鏈式隊列,開始時爲每個隊列先分配一個輸入緩衝區。另外建立空閒緩衝區的鏈式棧,把其餘 k 個空閒的緩衝區送入此棧中。輸出緩衝區OB定位於0號輸出緩衝區。
· 2、用 LastKey[i] 存放第 i 個歸併段最後輸入的關鍵碼,用 NextRun 存放 LastKey[i] 最小的歸併段段號;若有幾個 LastKey[i] 都 是最小時,將序號最小的 i 存放到 NextRun 中。如果LastKey [NextRun] ¹ ¥,則從空閒緩衝區棧中取一個空閒緩衝區,預先鏈入段號爲 NextRun 的歸併段的緩衝區隊列中。 ¸ 3、使用函數 kwaymerge 對 k 個輸入緩衝區隊列中的對象進行 k 路歸併,結果送入輸出緩衝區OB 中。歸併一直持續到輸出緩衝區 OB 變滿或者有一個關鍵碼爲 ¥ 的對象被歸併到OB 中 爲止。
如果一個輸入緩衝區變空,則 kwaymerge 進到該輸入緩衝區隊列中的下一個緩衝區,同時將變空的位於隊頭的緩衝區從隊列中退出,加入到空閒緩衝區棧中。 但如果在輸出緩衝區變滿或關鍵碼爲 ¥ 的對象被歸併到輸出緩衝區OB的同時一個輸入緩衝區變空,則 kwaymerge 不進到該輸入緩衝區隊列中的下一個緩衝區,變空的緩衝區也不從隊列中退出,歸併暫停。 ¹ 4、一直等着,直到磁盤輸入或磁盤輸出完成爲止,繼續歸併。
º 5、如果一個輸入緩衝區讀入完成,將它鏈入適當歸併段的緩衝區隊列中。然後確定滿足LastKey [NextRun] 的最小的 NextRun,確定下一步將讀入哪一個歸併段的對象。 » 6、如果 LastKey[NextRun] ¹ ¥,則從空閒緩衝區棧中取一個空閒緩衝區,從段號爲 NextRun 的歸併段中讀入下一塊,存放到這個空閒緩衝區中。 ¼ 7、開始寫出輸出緩衝區OB的對象,再將輸出緩衝區定位於1號輸出緩衝區。 ½ 8、如果關鍵碼爲 ¥ 的對象尚未被歸併到輸出緩衝區OB中,轉到¸繼續操作;否則,一直等待,直到寫出完成,然後算法結束。
n示例:假設對如下三個歸併段進行 3 路歸併。每個歸併段由3塊組成, 每塊有2個對象。各歸併段最後一個對象關鍵碼 爲∞。 u 歸併段1 { 20, 25 } { 26, 28 } { 36, ∞} u 歸併段2 { 23, 29 } { 34, 38 } { 70, ∞} u 歸併段3 { 24, 28 } { 31, 34 } { 50, ∞} n 設立6個輸入緩衝區,2個輸出緩衝區。利用動態緩衝算法,各歸併段輸入緩衝區隊列及輸出緩衝區狀態的變化如圖所示。
n 對於較大的k, 爲確定哪一個歸併段的輸入緩衝區最先變空,可對 LastKey[i], 0 <= i <= k-1,建立一棵敗者樹。通過 log2k 次比較就可確定哪一 個歸併段的緩衝區隊列最先變空。 n 對於較大的k,函數 kwaymerge 使用了敗者樹進行歸併。 n 除最初的 k 個輸入頁塊的讀入和最後一個輸出頁塊的寫出外,其它所有輸入,輸出和內部歸併都是並行執行的。此外,也有可能在 k 個歸併段歸併完後,需要立即開始對另外 k 個歸併段執行歸併。所以,在對 k 個歸併段進行歸併的最後階段,就開始下一批 k 個歸併段的輸入。 n 此算法假定所有的頁塊大小相同。
9.7.5 最佳歸併樹
n 歸併樹是描述歸併過程的 m 叉樹。因爲每一次做 m 路歸併都需要有 m 個歸併段參加,因此,歸併樹是隻有度爲0和度爲 m 的結點的正則 m 叉樹。 n示例:設有13個長度不等的初始歸併段,其長度(對象個數)分別爲 0, 0, 1, 3, 5, 7, 9, 13, 16, 20, 24, 30, 38 n其中長度爲 0 的是空歸併段。對它們進行 3 路歸併時的歸併樹如圖所示。 n在歸併樹中 u 各葉結點代表參加歸併的各初始歸併段 u 葉結點上的權值即爲該初始歸併段中的對象個 數 u 根結點代表最終生成的歸併段 u 葉結點到根結點的路徑長度表示在歸併過程中的讀對象次數 u 各非葉結點代表歸併出來的新歸併段 u 歸併樹的帶權路徑長度 WPL 即爲歸併過程中的總讀對象數。因而,在歸併過程中總的讀寫對象次數爲 2*WPL = 754。
n 不同的歸併方案所對應的歸併樹的帶權路徑長度各不相同。
n 爲了使得總的讀寫次數達到最少,需要改變歸併方案,重新組織歸併樹。 n 可將哈夫曼樹的思想擴充到 m 叉樹的情形。在歸併樹中,讓對象個數少的初始歸併段最先歸併,對象個數多的初始歸併段最晚歸併,就可以建立總的讀寫次數達到最少的最佳歸併樹。 n例如,假設有11個初始歸併段, 其長度(對象個數)分別爲 1, 3, 5, 7, 9, 13, 16, 20, 24, 30, 38 做3路歸併。
n爲使歸併樹成爲一棵正則三叉樹,可能需要補入空歸併段。補空歸併段的原則爲: u
n
n如果做5路歸併,讓 m = 5,則有 (11-1)/(5-1) = 2 n表示有2個度爲5的內結點;但是, u = (11-1) mod (5-1) =2 ¹ 0 n需要加一個內結點,它在歸併樹中代替了一個葉結點的位置,故一個葉結點參加這個內結點下的歸併,需要增加的空初始歸併段數爲 m-u-1=5-2-1 = 2 n應當補充2個空歸併段。則歸併樹如圖所示。 |