外排序-處理極大量數據的排序算法--5 億整數的大文件排序的思路

外排序

**外排序(External sorting)**是指能夠處理極大量數據的排序算法。通常來說,外排序處理的數據不能一次裝入內存,只能放在讀寫較慢的外存儲器(通常是硬盤)上。外排序通常採用的是一種“排序-歸併”的策略。在排序階段,先讀入能放在內存中的數據量,將其排序輸出到一個臨時文件,依此進行,將待排序數據組織爲多個有序的臨時文件。而後在歸併階段將這些臨時文件組合爲一個大的有序文件,也即排序結果。

外歸併排序

外排序的一個例子是外歸併排序(External merge sort),它讀入一些能放在內存內的數據量,在內存中排序後輸出爲一個順串(即是內部數據有序的臨時文件),處理完所有的數據後再進行歸併。比如,要對900 MB的數據進行排序,但機器上只有100 MB的可用內存時,外歸併排序按如下方法操作:

  1. 讀入100 MB的數據至內存中,用某種常規方式(如快速排序、堆排序、歸併排序等方法)在內存中完成排序。
  2. 將排序完成的數據寫入磁盤。
  3. 重複步驟1和2直到所有的數據都存入了不同的100 MB的塊(臨時文件)中。在這個例子中,有900 MB數據,單個臨時文件大小爲100 MB,所以會產生9個臨時文件。
  4. 讀入每個臨時文件(順串)的前10 MB( = 100 MB / (9塊 + 1))的數據放入內存中的輸入緩衝區,最後的10 MB作爲輸出緩衝區。(實踐中,將輸入緩衝適當調小,而適當增大輸出緩衝區能獲得更好的效果。)
  5. 執行九路歸併算法(見後面的多路排序),將結果輸出到輸出緩衝區。一旦輸出緩衝區滿,將緩衝區中的數據寫出至目標文件,清空緩衝區。一旦9個輸入緩衝區中的一個變空,就從這個緩衝區關聯的文件,讀入下一個10M數據,除非這個文件已讀完。這是“外歸併排序”能在主存外完成排序的關鍵步驟 – 因爲“歸併算法”(merge algorithm)對每一個大塊只是順序地做一輪訪問(進行歸併),每個大塊不用完全載入主存。

爲了增加每一個有序的臨時文件的長度,可以採用置換選擇排序(Replacement selection sorting)(可以可以先看下一項目錄)。它可以產生大於內存大小的順串。具體方法是在內存中使用一個最小堆進行排序,設該最小堆的大小爲M(對標前面的100M內存)。算法描述如下:

  1. 初始時將輸入文件讀入內存,建立最小堆。
  2. 將堆頂元素輸出至輸出緩衝區。然後讀入下一個記錄:
    1. 若該元素的關鍵碼值不小於剛輸出的關鍵碼值,將其作爲堆頂元素並調整堆,使之滿足堆的性質;
    2. 否則將新元素放入堆底位置,將堆的大小減1。(這裏wiki說的我不會實現,我覺得這裏應該是插入到另一個最小堆用於生成下一輪順串)
  3. 重複第2步,直至堆大小變爲0。
  4. 此時一個順串已經產生。將堆中的所有元素建堆(這裏我的想法就是想2.2中講的直接用另一個最小堆),開始生成下一個順串。

此方法能生成平均長度爲2M(對標等效於前面200M分文件)的順串,可以進一步減少訪問外部存儲器的次數(原本訪問兩次存儲器,現在平均只要訪問一次了),節約時間,提高算法效率。


注:個人理解可能有偏差,可能有錯誤,希望大佬們不吝賜教,感謝


置換選擇排序

我以前沒有看過這種算法的詳細描述,而是根據我從閱讀這套講義中學到的東西來進行分析的。

根據我的理解,選擇排序和替換選擇排序之間的主要區別在於,選擇排序旨在對存儲在主內存中的完整序列進行排序,而替換選擇排序則用於將太大而無法放入主內存的未排序序列轉換爲內存。可存儲在外部存儲器中的一系列“序列”排序序列。然後可以將這些外部鏈合併在一起以形成整體排序序列。儘管它們的名稱和算法操作的一兩個關鍵步驟相似,但它們的設計目的是解決根本不同的問題。

選擇排序
在線上有很多關於選擇排序的好教程,因此我不會花太多時間討論它。直觀上,該算法的工作方式如下:

找到最小的元素並將其交換到數組的位置0。
找到第二個最小的元素,並將其交換到數組的位置1。
找到第三小的元素並將其交換到數組的位置2

找到第n個最小的元素,並將其交換到數組的位置n-1。
這假設數組可以完全保存在內存中,如果是這種情況,該算法將以Θ(n 2)時間運行。這不是非常快,對於大型數據集則不建議使用。

替換選擇排序
該算法在1965年由Donald Knuth進行了描述,因此它的設計目的是在與我們目前所用的計算環境完全不同的計算環境中工作。計算機的內存很少(通常是一些固定數量的寄存器),但是可以訪問大型外部驅動器。通常會構建一些算法,將一些值加載到寄存器中,在其中進行處理,然後將其直接刷新回外部存儲。(有趣的是,這與處理器當前的工作方式類似,除了主存儲器而不是外部存儲器外)。

假設我們在內存中有足夠的空間來容納兩個數組:第一個Values大小爲n的數組可以容納一堆值,第二個Active大小爲n的數組可以容納布爾值。考慮到我們只有足夠的內存空間來容納Activeand Values數組,以及一些額外的存儲空間變量,我們將嘗試採用大量未排序值的輸入流,並盡力對它進行排序。

該算法背後的思想如下。首先,n將包含未排序序列的外部源的值直接加載到Values數組中。然後,將所有Active值設置爲true。例如,如果n = 4,我們可能具有以下設置:

Values: 4 1 0 3
Active: Yes Yes Yes Yes
替換選擇排序算法的工作原理是重複查找Values數組中的最小值並將其寫出到輸出流中。在這種情況下,我們首先查找0值並將其寫入流。這給

Values: 4 1 3
Active: Yes Yes Yes Yes

Output: 0
現在,我們在Values數組中有一個空白點,因此我們可以從外部源中提取另一個值。假設我們得到2。在這種情況下,我們有以下設置:

Values: 4 1 2 3
Active: Yes Yes Yes Yes

Output: 0
注意,由於2> 0且0是此處的最小元素,因此可以保證在將0寫入輸出時,2不應早於它。那很好。因此,我們繼續算法的下一步,並再次在此處找到最小的元素。那是1,所以我們將其發送到輸出設備:

Values: 4 2 3
Active: Yes Yes Yes Yes

Output: 0 1
現在,從外部源中讀取另一個值:

Values: 4 -1 2 3
Active: Yes Yes Yes Yes

Output: 0 1
現在我們有麻煩了。這個新值(-1)小於1,這意味着如果我們確實希望此值按排序順序進入輸出,則它應該位於1之前。但是,我們沒有足夠的內存來重新讀取輸出設備並進行修復。相反,我們將執行以下操作。現在,讓我們將-1保留在內存中。我們將盡最大努力對其餘元素進行排序,但是當我們這樣做時,我們將進行第二次迭代以生成排序後的序列,並將-1放入該序列中。換句話說,我們將產生兩個排序序列,而不是產生一個排序序列。

爲了在內存中表明我們尚不希望寫出-1,我們將標記-1的插槽標記爲非活動狀態。如圖所示:

Values: 4 -1 2 3
Active: Yes NO Yes Yes

Output: 0 1
從現在開始,我們將假裝-1不存在。

讓我們繼續前進。現在,我們在內存中找到仍處於活動狀態的最小值(2),並將其寫到設備中:

Values: 4 -1 3
Active: Yes NO Yes Yes

Output: 0 1 2
現在,我們從輸入設備中提取下一個值。假設它是7:

Values: 4 -1 7 3
Active: Yes NO Yes Yes

Output: 0 1 2
由於7> 2,它在輸出中的2之後,所以我們什麼也不做。

在下一次迭代中,我們找到最低的有效值(3)並將其寫出:

Values: 4 -1 7
Active: Yes NO Yes Yes

Output: 0 1 2 3
我們從輸入設備中提取下一個值。假設它也是 3。在這種情況下,我們知道3是最小的值,因此我們可以直接將其寫入輸出流,因爲3在這裏是所有值中的最小的,因此我們可以保存一個迭代:

Values: 4 -1 7
Active: Yes NO Yes Yes

Output: 0 1 2 3 3
現在,我們從輸入設備中提取下一個值。假設它是2。在這種情況下,和以前一樣,我們知道2應該排在3之前。就像前面的-1一樣,這意味着我們現在需要將2保留在內存中;我們稍後再寫出來。現在,我們的設置如下所示:

Values: 4 -1 7 2
Active: Yes NO Yes NO

Output: 0 1 2 3 3
現在,我們找到最小的有效值(4)並將其寫入輸出設備:

Values: -1 7 2
Active: Yes NO Yes NO

Output: 0 1 2 3 3 4
假設我們現在讀入1作爲下一個輸入。因此Values,我們將其放入,但將其標記爲無效:

Values: 1 -1 7 2
Active: NO NO Yes NO

Output: 0 1 2 3 3 4
只有一個活動值,即7,因此我們將其寫出:

Values: 1 -1 2
Active: NO NO Yes NO

Output: 0 1 2 3 3 4 7
假設我們現在讀取一個5。在這種情況下,和以前一樣,我們將其存儲但將插槽標記爲非活動:

Values: 1 -1 5 2
Active: NO NO NO NO

Output: 0 1 2 3 3 4 7
請注意,所有值現在都處於非活動狀態。這意味着我們已經從內存中清除了所有可以進入當前輸出運行的值。現在,我們需要去寫出稍後持有的所有值。爲此,我們將所有值標記爲活動值,然後像以前一樣重複:

Values: 1 -1 5 2
Active: Yes Yes Yes Yes

Output: 0 1 2 3 3 4 7
-1是最小值,因此我們將其輸出:

Values: 1 5 2
Active: Yes Yes Yes Yes

Output: 0 1 2 3 3 4 7 -1
假設我們讀取的是3. -1 < 3,因此我們將其加載到Values數組中。

Values: 1 3 5 2
Active: Yes Yes Yes Yes

Output: 0 1 2 3 3 4 7 -1
1是此處的最小值,因此我們將其刪除:

Values: 3 5 2
Active: Yes Yes Yes Yes

Output: 0 1 2 3 3 4 7 -1 1
假設我們現在沒有輸入值。我們將此插槽標記爲已完成:

Values: — 3 5 2
Active: Yes Yes Yes Yes

Output: 0 1 2 3 3 4 7 -1 1
接下來是2:

Values: — 3 5 —
Active: Yes Yes Yes Yes

Output: 0 1 2 3 3 4 7 -1 1 2
然後3:

Values: — — 5 —
Active: Yes Yes Yes Yes

Output: 0 1 2 3 3 4 7 -1 1 2 3
最後,5:

Values: — — — —
Active: Yes Yes Yes Yes

Output: 0 1 2 3 3 4 7 -1 1 2 3 5
我們完成了!請注意,結果序列未排序,但比以前好很多。現在,它由按排序順序的兩個鏈組成。將它們合併在一起(以與我們爲mergesort進行合併相同的方式)將對結果數組進行排序。該算法可能會產生更多的鏈,但是由於我們的樣本輸入很小,因此只有兩個。

那這有多快?好吧,循環的每次迭代最多進行n次比較(在內存中),一次讀取和一次寫入。因此,如果流中總共有N個值,則該算法執行O(nN)個比較和O(N)個存儲操作。如果內存操作很昂貴,那還算不錯,儘管最後需要第二遍才能將所有內容合併在一起。

在僞代碼中,算法如下所示:

Make Values an array of n elements.
Make Active an array of n booleans, all initially true.

Read n values from memory into Values.
Until no values are left to process:
    Find the smallest value that is still active.
    Write it to the output device.
    Read from the input device into the slot where the old element was.
    If it was smaller than the old element, mark the old slot inactive.
    If all slots are inactive, mark them all active.

如果現在有任何理由對此算法進行編碼,我會感到震驚。幾十年前,當內存真的很小的時候,這是有道理的。如今,有更好的外部排序算法可用(前面講的第一種方法),並且幾乎可以肯定它們的性能要優於該算法。(中文wiki說置換選擇排序方法減少了落磁盤,更快,這個人說第一種外歸併排序更快,但是我還是覺得減少落磁盤的可能置換選擇排序方法更快一點)

多路排序

定義

k條路

暴力法

每次選取一個min都從k個有序串中取出最前面的值來進行 k-1次min(a,b)

最佳歸併樹+敗者樹

  1. 在實現將初始文件分爲 m 個初始歸併段時,爲了儘量減小 m 的值,採用置換-選擇排序算法,可實現將整個初始文件分爲數量較少的長度不等的初始歸併段。
  2. 同時在將初始歸併段歸併爲有序完整文件的過程中,爲了儘量減少讀寫外存的次數,採用構建最佳歸併樹的方式,對初始歸併段進行歸併,而歸併的具體實現方法是採用敗者樹的方式。

可以輕度參考

參考鏈接

https://en.wikipedia.org/wiki/External_sorting
https://zh.wikipedia.org/wiki/%E5%A4%96%E6%8E%92%E5%BA%8F
https://stackoverflow.com/questions/16326689/replacement-selection-sort-v-selection-sort

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章