在一個文件中有10G個整數,亂序排列,要求找出中位數

題目:在一個文件中有 10G 個整數,亂序排列,要求找出中位數。內存限制爲 2G。只寫出思路即可(內存限制爲 2G的意思就是,可以使用2G的空間來運行程序,而不考慮這臺機器上的其他軟件的佔用內存)。

關於中位數:數據排序後,位置在最中間的數值。即將數據分成兩部分,一部分大於該數值,一部分小於該數值。中位數的位置:當樣本數爲奇數時,中位數=(N+1)/2 ; 當樣本數爲偶數時,中位數爲N/2與1+N/2的均值(那麼10G個數的中位數,就第5G大的數與第5G+1大的數的均值了)。

分析:明顯是一道工程性很強的題目,和一般的查找中位數的題目有幾點不同。
1. 原數據不能讀進內存,不然可以用快速選擇,如果數的範圍合適的話還可以考慮桶排序或者計數排序,但這裏假設是32位整數,仍有4G種取值,需要一個16G大小的數組來計數。

2. 若看成從N個數中找出第K大的數,如果K個數可以讀進內存,可以利用最小或最大堆,但這裏K=N/2,有5G個數,仍然不能讀進內存。

3. 接上,對於N個數和K個數都不能一次讀進內存的情況,《編程之美》裏給出一個方案:設k<K,且k個數可以完全讀進內存,那麼先構建k個數的堆,先找出第0到k大的數,再掃描一遍數組找出第k+1到2k的數,再掃描直到找出第K個數。雖然每次時間大約是nlog(k),但需要掃描ceil(K/k) 次,這裏要掃描5次。

解法:首先假設是32位無符號整數。
1. 讀一遍10G個整數,把整數映射到256M個區段中,用一個64位無符號整數給每個相應區段記數。
說明:整數範圍是0 - 2^32 - 1,一共有4G種取值,映射到256M個區段,則每個區段有16(4G/256M = 16)種值,每16個值算一段, 0~15是第1段,16~31是第2段,……2^32-16 ~2^32-1是第256M段。一個64位無符號整數最大值是0~8G-1,這裏先不考慮溢出的情況。總共佔用內存256M×8B=2GB。

2. 從前到後對每一段的計數累加,當累加的和超過5G時停止,找出這個區段(即累加停止時達到的區段,也是中位數所在的區段)的數值範圍,設爲[a,a+15],同時記錄累加到前一個區段的總數,設爲m。然後,釋放除這個區段佔用的內存。

3. 再讀一遍10G個整數,把在[a,a+15]內的每個值計數,即有16個計數。

4. 對新的計數依次累加,每次的和設爲n,當m+n的值超過5G時停止,此時的這個計數所對應的數就是中位數。

總結:
1.以上方法只要讀兩遍整數,對每個整數也只是常數時間的操作,總體來說是線性時間。

2. 考慮其他情況。
若是有符號的整數,只需改變映射即可。若是64爲整數,則增加每個區段的範圍,那麼在第二次讀數時,要考慮更多的計數。若過某個計數溢出,那麼可認定所在的區段或代表整數爲所求,這裏只需做好相應的處理。噢,忘了還要找第5G+1大的數了,相信有了以上的成果,找到這個數也不難了吧。

3. 時空權衡。
花費256個區段也許只是恰好配合2GB的內存(其實也不是,呵呵)。可以增大區段範圍,減少區段數目,節省一些內存,雖然增加第二部分的對單個數值的計數,但第一部分對每個區段的計數加快了(總體改變??待測)。

4. 映射時儘量用位操作,由於每個區段的起點都是2的整數冪,映射起來也很方便。

______________________________________________________________________________________

題目:在一個文件中有 10G 個整數,亂序排列,要求找出中位數。內存限制爲 2G。只寫出思路即可(內存限制爲 2G的意思就是,可以使用2G的空間來運行程序,而不考慮這臺機器上的其他軟件的佔用內存)。

 

關於中位數:數據排序後,位置在最中間的數值。即將數據分成兩部分,一部分大於該數值,一部分小於該數值。中位數的位置:當樣本數爲奇數時,中位數=(N+1)/2 ; 當樣本數爲偶數時,中位數爲N/2與1+N/2的均值(那麼10G個數的中位數,就第5G大的數與第5G+1大的數的均值了)。

 

分析: 既然要找中位數,很簡單就是排序的想法。那麼基於字節的桶排序是一個可行的方法 

思想:將整形的每1byte作爲一個關鍵字,也就是說一個整形可以拆成4個keys,而且最高位的keys越大,整數越大。如果高位keys相同,則比較次高位的keys。整個比較過程類似於字符串的字典序。

第一步:把10G整數每2G讀入一次內存,然後一次遍歷這536,870,912個數據。每個數據用位運算">>"取出最高8位(31-24)。這8bits(0-255)最多表示255個桶,那麼可以根據8bit的值來確定丟入第幾個桶。最後把每個桶寫入一個磁盤文件中,同時在內存中統計每個桶內數據的數量,自然這個數量只需要255個整形空間即可。

代價:(1) 10G數據依次讀入內存的IO代價(這個是無法避免的,CPU不能直接在磁盤上運算)。(2)在內存中遍歷536,870,912個數據,這是一個O(n)的線性時間複雜度。(3)把255個桶寫會到255個磁盤文件空間中,這個代價是額外的,也就是多付出一倍的10G數據轉移的時間。

第二步:根據內存中255個桶內的數量,計算中位數在第幾個桶中。很顯然,2,684,354,560個數中位數是第1,342,177,280個。假設前127個桶的數據量相加,發現少於1,342,177,280,把第128個桶數據量加上,大於1,342,177,280。說明,中位數必在磁盤的第128個桶中。而且在這個桶的第1,342,177,280-N(0-127)個數位上。N(0-127)表示前127個桶的數據量之和。然後把第128個文件中的整數讀入內存。(平均而言,每個文件的大小估計在10G/128=80M左右,當然也不一定,但是超過2G的可能性很小)。

代價:(1)循環計算255個桶中的數據量累加,需要O(M)的代價,其中m<255。(2)讀入一個大概80M左右文件大小的IO代價。

注意,變態的情況下,這個需要讀入的第128號文件仍然大於2G,那麼整個讀入仍然可以按照第一步分批來進行讀取。

第三步:繼續以內存中的整數的次高8bit進行桶排序(23-16)。過程和第一步相同,也是255個桶。

第四步:一直下去,直到最低字節(7-0bit)的桶排序結束。我相信這個時候完全可以在內存中使用一次快排就可以了。

 

 

整個過程的時間複雜度在O(n)的線性級別上(沒有任何循環嵌套)。但主要時間消耗在第一步的第二次內存-磁盤數據交換上,即10G數據分255個文件寫回磁盤上。一般而言,如果第二步過後,內存可以容納下存在中位數的某一個文件的話,直接快排就可以了。



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