1. 實現外部排序的兩個過程:html
2. 時間組成:算法
3. 爲了提升整個外部排序的效率,分別從以上兩個方面對外部排序進行了優化:數組
4. 優化遞進順序:性能
5 勝者樹 & 敗者樹 & 堆排序學習
發展歷史優化
堆:其實一開始就是隻有堆來完成多路歸併的,可是人們發現堆每次取出最小值以後,把最後一個數放到堆頂,調整堆的時候,每次都要選出父節點的兩個孩子節點的最小值,而後再用孩子節點的最小值和父節點進行比較,因此每調整一層須要比較兩次。
勝者樹:這時人們想可否簡化比較過程,這時就有了勝者樹,這樣每次比較只用跟本身的兄弟節點進行比較就好,因此用勝者樹能夠比堆少一半的比較次數。 而勝者樹在節點上升的時候首選須要得到父節點,而後再得到兄弟節點,而後再比較。spa
敗者樹:這時人們又想可否再次減小比較次數,因而就有了敗者樹。在使用敗者樹的時候,每一個新元素上升時,只須要得到父節點並比較便可。 .net
因此總的來講,減小了訪存的時間。 如今程序的主要瓶頸在於訪存了,計算倒幾乎能夠忽略不計了。3d
相同點指針
首先它們三個的相同點就是在於:空間和時間複雜度都是同樣的O(N*logN)。調整一次的時間複雜度都是O(logN)的。
因此這道題用堆來作,跟用敗者樹來作並無本質上的算法複雜度量級上的差異。
不一樣點
堆:全部的節點都是關鍵字; 每次調整一層須要比較兩次(父親 左孩子| 父親 右孩子)。
勝者樹:葉子節點是關鍵字,非葉子節點保存勝者索引;每次調整一層須要比較1次(本身 兄弟),讀取兩次(父親| 兄弟)。
敗者樹:葉子節點是關鍵字,非葉子節點保存敗者索引;每次調整一層須要比較1次(本身 父親),讀取一次(父親),只須要和路徑上的節點比較,不須要和兄弟節點比較,簡化了重構的過程。; 新增B[0]記錄比賽的勝者【在本例子中是ls[0]】
6. 涉及到的算法:
外部排序指的是大文件的排序,即待排序的記錄存儲在外存儲器上,待排序的文件沒法一次裝入內存,須要在內存和外部存儲器之間進行屢次數據交換,以達到排序整個文件的目的。
外部排序算法由兩個階段構成:預處理和合並排序。
例子1
給你一個包含20億個int類型整數的文件,計算機的內存只有2GB,怎麼給它們排序?一個int數佔4個字節,20個億須要80億字節,大概佔用8GB的內存,而計算機只有2GB的內存,數據都裝不下!能夠把8GB分割成4個2GB的數據來排,而後在把他們拼湊回去。以下圖:
在2G內存中排序的時候能夠選擇合適的內部排序,好比快速排序或歸併排序等算法。爲了方便,咱們把排序好的2G有序數據稱爲有序子串。接着把兩個小的有序子串合併成一個大的有序子串。
注意:讀取的時候是每次讀取一個int數,經過比較以後再輸出。按照這個方法來回合併,總共通過三次合併以後就能夠獲得8G的有序子串。
咱們假設須要排序的int數有12個,內存一次只能裝下3個int數。
接下來把12個數據分紅4份,而後排序成有序子串:
而後把子串進行兩兩合併:
輸出哪一個元素就在那個元素所在的有序子串再次讀入一個元素:
繼續
重複直到合併成一個包含6個int有序子串:
再把兩個包含6個int的有序子串合併成一個包含12個int數據的最終有序子串:
由於硬盤的讀寫速度比內存要慢的多,按照以上這種方法,每一個數據都從硬盤讀了三次,寫了三次,要花不少時間。
解釋下:例如對於數據2,咱們把無序的12個數據分紅有序的4個子串須要讀寫各一次,把2份3個有序子串合併成6個有序子串讀寫各一次;把2份6個有序子串合併從12個有序子串讀寫各一次,一共須要讀寫各3次。
在進行有序子串合併的時候,不採起兩兩合併的方法,而是能夠3個子串,或4個子串一塊兒來合併。
例子2
例如,有一個含有 10000 個記錄的文件,可是內存的可以使用容量僅爲 1000 個記錄,毫無疑問須要使用外部排序算法,具體分爲兩步:
注意:此例中採用了將文件進行等分的操做,還有不等分的算法,後面會介紹。
如圖 1 所示有 10 個初始歸併段到一個有序文件,共進行了 4 次歸併,每次都由 m 個歸併段獲得 ⌈m/2⌉ 個歸併段,這種歸併方式被稱爲 2-路平衡歸併。
注意:在實際歸併的過程當中,因爲內存容量的限制不能知足同時將 2 個歸併段所有完整的讀入內存進行歸併,只能不斷地取 2 個歸併段中的每一小部分進行歸併,經過不斷地讀數據和向外存寫數據,直至 2 個歸併段完成歸併變爲 1 個大的有序文件。
對於外部排序算法來講,影響總體排序效率的因素主要取決於讀寫外存的次數,即訪問外存的次數越多,算法花費的時間就越多,效率就越低。
對於同一個文件來講,對其進行外部排序時訪問外存的次數同歸並的次數成正比,即歸併操做的次數越多,訪問外存的次數就越多。3.1 中圖 1 中使用的是 2-路平衡歸併的方式,觸類旁通,還可使用 3-路歸併、4-路歸併甚至是 10-路歸併的方式。
例子1
咱們假設內存一共能夠裝4個int型數據。
剛纔咱們是採起兩兩合併的方式,如今咱們能夠採起4個有序子串一塊兒合併的方式,這樣的話,每一個數據從硬盤讀寫的次數各須要2次就能夠了。如圖:
4個有序子串的合併,叫4路歸併。若是是n個有序子串的合併,就把它稱爲n路歸併。n並不是越大越好。N越大,內部排序所須要的時間越多。
例子2
圖 2 爲 5-路歸併的方式:
對比3.1 中 圖 1 和 3.2 中圖 2能夠看出,對於 k-路平衡歸併中 k 值得選擇,增長 k 能夠減小歸併的次數,從而減小外存讀寫的次數,最終達到提升算法效率的目的。除此以外,通常狀況下對於具備 m 個初始歸併段進行 k-路平衡歸併時,歸併的次數爲:s=⌊logkm ⌋(其中 s 表示歸併次數)。
從公式上能夠判斷出,想要達到減小歸併次數從而提升算法效率的目的,能夠從兩個角度實現:
其增長 k 值的想法引伸出了一種外部排序算法:多路平衡歸併算法;增長數量 m 的想法引伸出了另外一種外部排序算法:置換-選擇排序算法。
對於外部排序算法來講,其直接影響算法效率的因素爲讀寫外存的次數,即次數越多,算法效率越低。若想提升算法的效率,即減小算法運行過程當中讀寫外存的次數,能夠增長 k –路平衡歸併中的 k 值。可是通過計算得知,若是毫無限度地增長 k 值,雖然會減小讀寫外存數據的次數,但會增長內部歸併的時間,得不償失。(k越大,內部歸併排序【好比選出最小值】須要花費更多的時間,因此k不是越大越好)
例如在上節中,對於 10 個臨時文件,當採用 2-路平衡歸併時,若每次從 2 個文件中想獲得一個最小值時只需比較 1 次;而採用 5-路平衡歸併時,若每次從 5 個文件中想獲得一個最小值就須要比較 4 次。以上僅僅是獲得一個最小值記錄,如要獲得整個臨時文件,其耗費的時間就會相差很大。
爲了不在增長 k 值的過程當中影響內部歸併的效率,在進行 k-路歸併時可使用「敗者樹」來實現,該方法在增長 k 值時不會影響其內部歸併的效率。
勝者樹和敗者樹都是徹底二叉樹(非葉子節點存儲的是索引),他們是樹形選擇排序的變形(非葉子節點存儲的是具體的值)。
當咱們將咱們的勝者樹的最優值輸入到咱們的輸出緩衝區(輸出緩衝區從內存中額外開闢出來的一段,咱們存儲當前的歸併的結果,緩衝區滿寫入磁盤)
以後,咱們的根節點便出現了空的狀況,咱們須要從根節點對應的輸入緩衝區中在讀入一個數據來充當下一次比較的選手,而後從下到上進行維護,咱們的每一次的維護都須要比較兄弟的勝者而後選出新一輪的勝者而後一直優化到咱們的根的路徑上(從低至上,貫穿整個樹)以後咱們不斷地進行上述的操做,指導咱們的全部的輸入緩衝區已經爲空爲止。
例子:
勝者樹的一個優勢是,若是一個選手的值改變了,能夠很容易地修改這棵勝者樹。只須要沿着從該結點到根結點的路徑修改這棵二叉樹,而沒必要改變其餘比賽的結果。
咱們把勝者樹分爲兩部分:
b[]:用來保存K路數組的首元素,葉節點存放在此處,即底下那七個數組
ls[]:用來保存勝者數組的下標,ls[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。
葉子結點b3的值變爲11時,重構的勝者樹如圖所示
1. b3 PK b4,b3勝b4負,內部結點ls[4]的值爲3;
2. b3 PK b0,b0勝b3負,內部結點ls[2]的值爲0;
3. b1 PK b2,b1勝b2負,內部結點ls[3]的值爲1;
4. b0 PK b1,b1勝b0負,內部結點ls[1]的值爲1。.
咱們的勝者樹維護的時候每次都須要去查找咱們的根的兄弟節點的位置來進行比較,可是咱們的每一次都要多一步查找兄弟的劃,不管是對咱們的程序的實現過程仍是咱們的時間效率上來看都還存在改進的餘地。這裏咱們就要引入敗者樹,敗者樹與勝者樹剛好相反,其雙親結點存儲的是左右孩子比較以後的失敗者,而勝利者則繼續同其它的勝者去比較。
敗者樹的定義:
比勝過程
例子1:
敗者樹是勝者樹的一種變體。在敗者樹中,用父結點記錄其左右子結點進行比賽的敗者,而讓勝者參加下一輪的比賽。敗者樹的根結點記錄的是敗者,須要加一個結點來記錄整個比賽的勝利者。採用敗者樹能夠簡化重構的過程。
咱們把敗者樹分爲兩部分:
b[]:用來保存K路數組的首元素,葉節點存放在此處,即底下那七個數組
ls[]:用來保存敗者數組的下標,b[0]是最終的勝者(即所求的數),敗者節點存放在中間節點。
敗者樹的中間結點記錄的敗者的標號
敗者樹示例,規定數大者敗。
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]中。
是當b3變爲13時,敗者樹的重構圖:
注意,敗者樹的重構跟勝者樹是不同的,敗者樹的重構只須要與其父結點比較。b3與結點ls[4]的原值比較,ls[4]中存放的原值是結點4,即b3與b4比較,b3負b4勝,則修改ls[4]的值爲結點3。同理,以此類推,沿着根結點不斷比賽,直至結束。
例子2:
例如仍是圖 1 中,葉子結點 49 和 38 比較,38 更小,因此 38 是勝利者,49 爲失敗者,但因爲是敗者樹,因此其雙親結點存儲的應該是 49;一樣,葉子結點 65 和 97 比較,其雙親結點中存儲的是 97 ,而 65 則用來同 38 進行比較,65 會存儲到 97 和 49 的雙親結點的位置,38 繼續作後續的勝者比較,依次類推。
勝者樹和敗者樹的區別就是:勝者樹中的非終端結點中存儲的是勝利的一方;而敗者樹中的非終端結點存儲的是失敗的一方。而在比較過程當中,都是拿勝者去比較。
如圖 2 所示爲一棵 5-路歸併的敗者樹,其中 b0—b4 爲樹的葉子結點,分別爲 5 個歸併段中存儲的記錄的關鍵字。ls 爲一維數組,表示的是非終端結點,其中存儲的數值表示第幾歸併段(例如 b0 爲第 0 個歸併段)。ls[0] 中存儲的爲最終的勝者,表示當前第 3 歸併段中的關鍵字最小。
當最終勝者判斷完成後,只須要更新葉子結點 b3 的值,即導入關鍵字 15,而後讓該結點不斷同其雙親結點所表示的關鍵字進行比較,敗者留在雙親結點中,勝者繼續向上比較。
例如,葉子結點 15 先同其雙親結點 ls[4] 中表示的 b4 中的 12 進行比較,12 爲勝利者,則 ls[4] 改成失敗者 15 所在的歸併段即 b3,而後 12 繼續同 ls[2] 中表示的 10 作比較,10 爲勝者,則 ls[2] 改成失敗者 12 所在的歸併段即 b4,而後 10 繼續同其雙親結點 ls[1] 表示的 b1(關鍵字 9)做比較,最終 9 爲勝者。整個過程以下圖所示:
注意:爲了防止在歸併過程當中某個歸併段變爲空,處理的辦法爲:能夠在每一個歸併段最後附加一個關鍵字爲最大值的記錄。這樣當某一時刻選出的冠軍爲最大值時,代表 5 個歸併段已所有歸併完成。(由於只要還有記錄,最終的勝者就不多是附加的最大值)
本節介紹了經過使用敗者樹來實現增長 k-路歸併的規模來提升外部排序的總體效率。可是對於 k 值得選擇也並非一味地越大越好,而是須要綜合考慮選擇一個合適的 k 值。
發展歷史
堆:其實一開始就是隻有堆來完成多路歸併的,可是人們發現堆每次取出最小值以後,把最後一個數放到堆頂,調整堆的時候,每次都要選出父節點的兩個孩子節點的最小值,而後再用孩子節點的最小值和父節點進行比較,因此每調整一層須要比較兩次。
勝者樹:這時人們想可否簡化比較過程,這時就有了勝者樹,這樣每次比較只用跟本身的兄弟節點進行比較就好,因此用勝者樹能夠比堆少一半的比較次數。 而勝者樹在節點上升的時候首選須要得到父節點,而後再得到兄弟節點,而後再比較。
敗者樹:這時人們又想可否再次減小比較次數,因而就有了敗者樹。在使用敗者樹的時候,每一個新元素上升時,只須要得到父節點並比較便可。
因此總的來講,減小了訪存的時間。 如今程序的主要瓶頸在於訪存了,計算倒幾乎能夠忽略不計了。
相同點
首先它們三個的相同點就是在於:空間和時間複雜度都是同樣的O(N*logN)。調整一次的時間複雜度都是O(logN)的。
因此這道題用堆來作,跟用敗者樹來作並無本質上的算法複雜度量級上的差異。
不一樣點
堆:全部的節點都是關鍵字; 每次調整一層須要比較兩次(父親 左孩子| 父親 右孩子)。
勝者樹:葉子節點是關鍵字,非葉子節點保存勝者索引;每次調整一層須要比較1次(本身 兄弟),讀取兩次(父親| 兄弟)。
敗者樹:葉子節點是關鍵字,非葉子節點保存敗者索引;每次調整一層須要比較1次(本身 父親),讀取一次(父親),只須要和路徑上的節點比較,不須要和兄弟節點比較,簡化了重構的過程。; 新增B[0]記錄比賽的勝者【在本例子中是ls[0]】
k 不是越大越好,那麼咱們能夠想辦法減小有序子串的總個數 m。這樣,也能減小數據從硬盤讀寫的次數。
上一節介紹了增長 k-路歸併排序中的 k 值來提升外部排序效率的方法,而除此以外,還有另一條路可走,即減小初始歸併段的個數,也就是本章第一節中提到的減少 m 的值。
m 的求值方法爲:m=⌈n/l⌉(n 表示爲外部文件中的記錄數,l 表示初始歸併段中包含的記錄數)
若是要想減少 m 的值,在外部文件總的記錄數 n 值必定的狀況下,只能增長每一個歸併段中所包含的記錄數 l。而對於初始歸併段的造成,就不能再採用上一章所介紹的內部排序的算法,由於全部的內部排序算法正常運行的前提是全部的記錄都存在於內存中,而內存的可以使用空間是必定的,若是增長 l 的值,內存是盛不下的。因此要另想它法,探索一種新的排序方法:置換—選擇排序算法。
例如已知初始文件中總共有 24 個記錄,假設內存工做區最多可容納 6 個記錄,按照以前的選擇排序算法最少也只能分爲 4 個初始歸併段。而若是使用置換—選擇排序,能夠實現將 24 個記錄分爲 3 個初始歸併段,如圖 1 所示:
置換—選擇排序算法的具體操做過程爲:
拿圖 1 中的初始文件爲例,首先輸入前 6 個記錄到內存工做區,其中關鍵字最小的爲 29,因此選其爲 MINIMAX 記錄,同時將其輸出到歸併段文件中,以下圖所示:
此時初始文件不爲空,因此從中輸入下一個記錄 14 到內存工做區中,而後從內存工做區中的比 29 大的記錄中,選擇一個最小值做爲新的 MINIMAX 值輸出到 歸併段文件中,以下圖所示:
初始文件還不爲空,因此繼續輸入 61 到內存工做區中,從內存工做區中的全部關鍵字比 38 大的記錄中,選擇一個最小值做爲新的 MINIMAX 值輸出到歸併段文件中,以下圖所示:
如此重複性進行,直至選不出 MINIMAX 值爲止,以下圖所示:
當選不出 MINIMAX 值時,表示一個歸併段已經生成,則開始下一個歸併段的建立,建立過程同第一個歸併段同樣,這裏再也不贅述。
咱們要如何從內存中選出這個目的數呢?難道每次都把內存中的數據進行排序,而後再逐個比較選擇嗎?其實咱們能夠構建一個最小堆來幫助咱們選擇目的數。具體以下:
12個無序的數
從12個數據中讀取3個數據,構建一個最小堆,而後從堆頂選擇一個數寫入到p1中。以後再從剩餘的9個數中讀取一個數,若是這個數比剛纔那個寫入到p1中的數大,則把這個數插入到最小堆中,從新調整最小堆結構,而後在堆頂選一個數寫入到p1中。不然,把這個數暫放在一邊,暫時不處理。以後同樣須要調整堆結構,從堆頂選擇一個數寫入到p1中。這裏說明一下,那個被放在一邊的數是不能在放入p1中的了,由於它必定比p1中的數都要小,因此它會放在下一個子串中。以下圖所示:
從12個數據中讀取3個數據:
構建最小堆,且選出目標數:
讀入下一個數86:
讀入下一個數3,比70小,暫放一邊,不加入堆結構中:
讀入下一個數據24,比81小,不加入堆結構:
讀入下一個數據8,比86小,不加入堆結構。此時p1已經完成了,把那些剛纔暫放一邊的數從新構成一個堆,繼續p2的存放:
以此類推…最後生成的p2以下:
這樣子的話,最後只生成了2個有序子串,咱們把這種方法稱之爲置換選擇。按照這種方法,最好的狀況下,全部數據只生成一個有序子串;最壞的狀況下,和原來沒采起置換選擇算法同樣,仍是4個子串,那平均性能如何呢?
結論:若是內存能夠容納n個元素的話,那麼平均每一個子串的長度爲2m,也就是說,使用置換選擇算法咱們能夠減小一半的子串數。
在上述建立初始段文件的過程當中,須要不斷地在內存工做區中選擇新的 MINIMAX 記錄,即選擇不小於舊的 MINIMAX 記錄的最小值,此過程須要利用「敗者樹」來實現。
同上一節所用到的敗者樹不一樣的是,在不斷選擇新的 MINIMAX 記錄時,爲了防止新加入的關鍵字值小的的影響,每一個葉子結點附加一個序號位,當進行關鍵字的比較時,先比較序號,序號小的爲勝者;序號相同的關鍵字值小的爲勝者。
在初期建立敗者樹時也能夠經過不斷調整敗者樹的方式,其中全部記錄的序號均設爲 0 ,而後從初始文件中逐個輸入記錄到內存工做區中,自下而上調整敗者樹。過程以下:
提示:敗者樹根結點上方的方框內表示的爲最終的勝者所處的位置。
提示:序號 1 爲敗者。 先比較序號,序號小的爲勝者;序號相同的關鍵字值小的爲勝者。
由敗者樹得知,其最終勝者爲 29,設爲 MINIMAX 值,將其輸出到初始歸併文件中,同時再讀入下一個記錄 14,調整敗者樹,以下圖所示:
經過不斷地向敗者樹中讀入記錄,會產生多個 MINIMAX,直到最終全部葉子結點中的序號都爲 2,此時產生的新的 MINIMAX 值的序號 2,代表此歸併段生成完成,而此新的 MINIMAX 值就是下一個歸併段中的第一個記錄。
經過置換選擇排序算法獲得的初始歸併段,其長度並不會受內存容量的限制,且經過證實得知使用該方法所得到的歸併段的平均長度爲內存工做區大小的兩倍。
經過對初始文件進行置換選擇排序可以得到多個長度不等的初始歸併段
證實此結論的方法是 E.F.Moore(人名)在 1961 年從置換—選擇排序和掃雪機的類比中得出的,有興趣的能夠本身瞭解一下。
若不計輸入輸出的時間,經過置換選擇排序生成初始歸併段的所需時間爲O(nlogw)
(其中 n 爲記錄數,w 爲內存工做區的大小)。
經過上一節對置換-選擇排序算法的學習瞭解到,經過對初始文件進行置換選擇排序可以得到多個長度不等的初始歸併段,相比於按照內存容量大小對初始文件進行等分,大大減小了初始歸併段的數量,從而提升了外部排序的總體效率。
本節帶領你們思考一個問題:不管是經過等分仍是置換-選擇排序獲得的歸併段,如何設置它們的歸併順序,可使得對外存的訪問次數降到最低?
例如,現有經過置換選擇排序算法所獲得的 9 個初始歸併段,其長度分別爲:9,30,12,18,3,17,2,6,24
。在對其採用 3-路平衡歸併的方式時可能出現如圖 1 所示的狀況:
提示:圖 1 中的葉子結點表示初始歸併段,各自包含記錄的長度用結點的權重來表示;非終端結點表示歸併後的臨時文件。
假設在進行平衡歸併時,操做每一個記錄都須要單獨進行一次對外存的讀寫,那麼圖 1 中的歸併過程須要對外存進行讀或者寫的次數爲:(9+30+12+18+3+17+2+6+24)*2*2=484(圖 1 中涉及到了兩次歸併,對外存的讀和寫各進行 2 次)從計算結果上看,對於圖 1 中的 3 叉樹來說,其操做外存的次數剛好是樹的帶權路徑長度的 2 倍。因此,對於如何減小訪問外存的次數的問題,就等同於考慮如何使 k-路歸併所構成的 k 叉樹的帶權路徑長度最短。若想使樹的帶權路徑長度最短,就是構造赫夫曼樹。
在學習赫夫曼樹時,只是涉及到了帶權路徑長度最短的二叉樹爲赫夫曼樹,其實擴展到通常狀況,對於 k 叉樹,只要其帶權路徑長度最短,亦能夠稱爲赫夫曼樹。
若對上述 9 個初始歸併段構造一棵赫夫曼樹做爲歸併樹,如圖 2 所示:
依照圖 2 所示,其對外存的讀寫次數爲:(2*3+3*3+6*3+9*2+12*2+17*2+18*2+24*2+30)*2=446
經過以構建赫夫曼樹的方式構建歸併樹,使其對讀寫外存的次數降至最低(k-路平衡歸併,須要選取合適的 k 值,構建赫夫曼樹做爲歸併樹)。因此稱此歸併樹爲最佳歸併樹。
上述圖 2 中所構建的爲一顆真正的 3叉樹(樹中各結點的度不是 3 就是 0),而若 9 個初始歸併段改成 8 個,在作 3-路平衡歸併的時候就須要有一個結點的度爲 2。
對於具體設置哪一個結點的度爲 2,爲了使總的帶權路徑長度最短,正確的選擇方法是:附加一個權值爲 0 的結點(稱爲「虛段」),而後再構建赫夫曼樹。例如圖 2 中若去掉權值爲 30 的結點,其附加虛段的最佳歸併樹如圖 3 所示:
注意:虛段的設置只是爲了方便構建赫夫曼樹,在構建完成後虛段自動去掉便可。
對於如何判斷是否須要增長虛段,以及增長多少虛段的問題,有如下結論直接套用便可:在通常狀況下,對於 k–路平衡歸併來講,若 (m-1) MOD (k-1) = 0,則不須要增長虛段;不然需附加 k - (m-1)MOD(k-1) - 1 個虛段。
爲了提升整個外部排序的效率,分別從以上兩個方面對外部排序進行了優化:
發展歷史
堆:其實一開始就是隻有堆來完成多路歸併的,可是人們發現堆每次取出最小值以後,把最後一個數放到堆頂,調整堆的時候,每次都要選出父節點的兩個孩子節點的最小值,而後再用孩子節點的最小值和父節點進行比較,因此每調整一層須要比較兩次。
勝者樹:這時人們想可否簡化比較過程,這時就有了勝者樹,這樣每次比較只用跟本身的兄弟節點進行比較就好,因此用勝者樹能夠比堆少一半的比較次數。 而勝者樹在節點上升的時候首選須要得到父節點,而後再得到兄弟節點,而後再比較。
敗者樹:這時人們又想可否再次減小比較次數,因而就有了敗者樹。在使用敗者樹的時候,每一個新元素上升時,只須要得到父節點並比較便可。
因此總的來講,減小了訪存的時間。 如今程序的主要瓶頸在於訪存了,計算倒幾乎能夠忽略不計了。
相同點
首先它們三個的相同點就是在於:空間和時間複雜度都是同樣的O(N*logN)。調整一次的時間複雜度都是O(logN)的。
因此這道題用堆來作,跟用敗者樹來作並無本質上的算法複雜度量級上的差異。
不一樣點
堆:全部的節點都是關鍵字; 每次調整一層須要比較兩次(父親 左孩子| 父親 右孩子)。
勝者樹:葉子節點是關鍵字,非葉子節點保存勝者索引;每次調整一層須要比較1次(本身 兄弟),讀取兩次(父親| 兄弟)。
敗者樹:葉子節點是關鍵字,非葉子節點保存敗者索引;每次調整一層須要比較1次(本身 父親),讀取一次(父親),只須要和路徑上的節點比較,不須要和兄弟節點比較,簡化了重構的過程。; 新增B[0]記錄比賽的勝者【在本例子中是ls[0]】