從大量資料中取得前100個最大的演算法

2021-06-19 06:11:25 字數 3398 閱讀 5014

概括:用陣列實現的帶頭尾指標的雙向減序鍊錶,使用插入排序,並通過中位數下標來決定是使用尾部插入還是頭部插入的,實現從大量資料(儲存於檔案)中提取其最大100個的演算法。優點,讀取檔案一遍,空間的非動態申請,無陣列插入排序的大量"後移"操作,避免有序檔案帶來的最差表現。

解決過程:好久不用考慮演算法了,突然這麼乙個問題,頭腦有點空,只好點開一本講演算法的電子書,從目錄開始,第一章是演算法的複雜性度量,然後是資料結構,然後是排序,一些相關的知識猶如從硬碟載入記憶體。問題的相關知識被串起來。覺得這本身是個排序問題,排序演算法一大堆,但基本操作都是比較和賦值(+空間分配)。回顧各種排序演算法的特點後,覺得插入排序,即每次插入到合適的位置,比較符合。主要是每次插入後都能得到乙個有序的「陣列」供後續陣列插入,而插入乙個元素到乙個有序陣列,有些特殊的優勢,總結來說是減少比較次數:例如對乙個減陣列,你先比較後面的(較小的資料),如果要插入的資料比當前比較位置的資料都要小,就可以停止比較並進行插入,繼而轉向下乙個資料的插入了。雖然這裡會引發乙個不容易注意的問題,原來資料的有序的程度會導致排序時間的極大波動,這裡的這裡測試的是10倍左右,[38s , 5min30s].迫使後面不得不引進中位數來決定當前待插入元素究竟是「頭插」,還是「尾插」。由於這裡只要前100個最大的,意味著我只以需考慮(挑選出)最大的100個,佔10萬分之一,而至於剩下的0.99999都可以忽略(被覆蓋,不出現在最終結果陣列中的100個中)。相比其他經典的排序演算法,以上兩點是選擇插入排序演算法的主要原因。

有了演算法,還得有資料結構,「程式=演算法+資料結構」,儘管以上的考慮都是以陣列為例子(考慮場景)的。本質使用的是線性資料結構的特點(類似於諜戰中的單線聯絡),然而具備線性結構的除陣列以外還有鍊錶,陣列靠空間相連來實現(指示)線性表中前後關係,而鍊錶靠指標域來實現,以指標域的特點來劃分,鍊錶除單鏈表以外,還有迴圈和雙鏈表,對應到陣列,陣列也具備這樣的劃分,或者說可以實現相應的功能,如果不深究,如下表述是正確的,通常的陣列遍歷類似單鏈表的特點,通過取模運算((i++)%(sizeof(array)/sizeof(array[0]));)實現迴圈鍊錶的迴圈特點,至於雙向特性,是陣列的天生優點,i-1,意味著this->prev,i+1,意味著this->next;

選哪個好呢,陣列?,怎麼實現,不能,又缺少什麼?還是鍊錶,具體些,是單鏈表,迴圈還是雙向鍊錶?又會有什麼問題。

如果用陣列,每次插入會帶來平均為50次的 「搬動」,如果用單鏈表,會帶來接近1000萬的malloc和free的操作,儘管這種操作(系統呼叫)單個可能很快。迴圈鍊錶似乎和單鏈表沒有多大區別。至於雙向鍊錶,不是有「頭插」和 「尾插」的區別嗎,看樣子應該利用 「雙向」的特性, 「可是...」,你可能會問到,「難道雙向鍊錶就不需要那麼多的malloc 和free了?」,確實,想必,你已猜到,使用完100個空間後,插入第101個元素時,讓其覆蓋前100個中的最小的元素,插入的時機是在找到其合適的位置,例如在乙個已建立的100個元素的遞減的雙向鍊錶中,從尾部依次往前比較,直到找到正確的位置p(指標),則把當前元素覆蓋掉尾指標指向的元素,並把該空間從鍊錶中斷開,並把該空間插入p和p->prev之間的位置。想到這,程式就可以寫出來的,只是要注意一些邊界問題。不過真動手寫時,這裡有兩個技巧值得你借鑑,一是分階段處理,二是引入邊界值。

分階段處理,是指將第一階段先排100個元素,把100個空間用完,第二階段在原來的空間用完的基礎上插入餘下的元素,然後在每個階段單獨考慮,以此減少編寫的難度。

引入邊界值,是引入max和min ,並把迴圈鍊錶初始為只有這兩個元素,併排好序.min ,max 取值為對應欄位的邊界值或應用中的邊界值,例如在我的系統32位,redhat6上,採用編譯器預定義的巨集 __int_max__ 作為max ,其相反數作為min.這樣的好處是可以統一條件判斷,和減少初始情況判斷。因為在對乙個int數而言,一定是在max和min之間。

問題到這似乎解決了,新問題出現了:

以下實驗中的要建立的都是遞減(非遞增)的序列。

實驗1:首先單獨讀取,不排序讀取乙個隨機數大檔案花費時間為34s.

[root@localhost tmp]# ll ./r.dat

----------. 1 root root 1073741824 11月 18 12:45 ./r.dat

[root@localhost tmp]# time cat ./r.dat > /dev/null

real 0m34.775s

user 0m0.003s

sys 0m2.102s

實驗2:再(1024*64)byte大小緩衝區(通過巨集定義,改變時需重新編譯)讀取該檔案,派出前100個花費時間是 4分52秒

[root@localhost tmp]# time ./pick 1073741824 ./r.dat >/dev/null

real 4m52.041s

user 3m55.498s

sys 0m6.049s

實驗3:減少緩衝區為128byte時,花費5分48秒,與實驗2相比,增大緩衝區還是有優勢,

[root@localhost tmp]# time ./pick 1073741824 ./r.dat >/dev/null

real 5m48.831s

user 4m3.556s

sys 0m19.492s

實驗4:以(1024*64)讀取,通過「前插」法,對遞增檔案進行排序,時間接近實驗1不排序的結果,而「後插法」對隨機檔案的排序也是接近實驗1不排序的時間。

這三者的時間都接近30秒和實驗二的4分52比較說明檔案的有序情況影響著前叉和後插的效率,具體來說。 「前插法」 在對減序檔案排序時有最差表現, 「後插」對增序檔案排序時有最差表現。另外在實驗中發現的對於普通的隨機數檔案,「後插法」遠比 「前插法「 有效的的原因是因為,這裡需要的是前100個最大的,只佔1000萬個的極小一部,而且有維護被插入的序列是減序前提,因而對於1000萬中的0.9999的資料而言,採用  「後插法」 時,只比較了1次,就被判斷不在前一百個中,而被跳過去了。

[root@localhost tmp]# time ./pick 1073741824 ./r.dat >/dev/null 1

real 0m33.782s

user 0m1.000s

sys 0m4.370s

為了避免這種由於檔案有序程度帶來的程式執行時間的波動,要麼維護檔案的有序性,要麼修改演算法(維護減序序列的前提不變):

維護乙個中位數指標mid(或者陣列索引,如果採用陣列實現的雙向鍊錶的話),其初始化是在第一階段後,即有100個資料被插入到鍊錶中,使該指標指向第50(或49)個元素,

其後每個元素插入之前都與該指標所指向結構的data域相比較,若待插入元素大,則使用前插,並且mid=mid->prev,否則使用尾插,並且mid=mid->next,當然如果被插入的位置剛好是mid所指,則不用移動。

typedef struct  taghead

int data;

unsigned short prev;

unsigned short next;

}head;

head head[100];

從100億隨機數中找到前100萬個最大的數

面試問到的一道題目 主要步驟 一 使用乙個大小為一百萬零一的整數陣列來構建堆 堆的下標從1開始 二 從檔案中讀取前一百萬個數,每讀入乙個數,呼叫函式,保持其最小堆的性質,堆的根永遠是堆中最小的元素。三 從一百萬零乙個數開始,每讀入乙個數開始,比較這個數與堆的根比較,如果比根大,就用這個數替換掉根,呼...

在100w個數中找最大的前100個數

1.演算法如下 根據快速排序劃分的思想 1 遞迴對所有資料分成 a,b b b,d 兩個區間,b,d 區間內的數都是大於 a,b 區間內的數 2 對 b,d 重複 1 操作,直到最右邊的區間個數小於100個。注意 a,b 區間不用劃分 3 返回上乙個區間,並返回此區間的數字數目。接著方法仍然是對上一...

堆排序思想 找出100萬個資料中的前100大資料

首先在c 的檔案下新建乙個txt檔案,命名為test.txt,然後在generate random.cpp中寫如下 先讀取txt檔案中生成的100萬個隨機數,這個量是很大的 其實可以生成更多,這裡用100w為例 如果完整排序後輸出前100大,這樣非常耗時!所以選擇如下演算法 構造乙個容量為100的小...