題目和基本思路都來源網上,本人加以整理。
題目:在一個文件中有 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的整數冪,映射起來也很方便。
答案:
1, 把整數分成256M段,每段可以用64位整數保存該段數據個數,256M*8 = 2G內存,先清0
2,讀10G整數,把整數映射到256M段中,增加相應段的記數
3,掃描256M段的記數,找到中位數的段和中位數的段前面所有段的記數,可以把其他段的內存釋放
4,因中位數段的可能整數取值已經比較小(如果是32bit整數,當然如果是64bit整數的話,可以再次分段),對每個整數做一個記數,再讀一次10G整數,只讀取中位數段對應的整數,並設置記數。
5,對新的記數掃描一次,即可找到中位數。
如果是32bit整數,讀10G整數2次,掃描256M記數一次,后一次記數因數量很小,可以忽略不記
(設是32bit整數,按無符號整數處理
整數分成256M段? 整數范圍是0 - 2^32 - 1 一共有4G種取值,4G/256M = 16,每16個數算一段 0-15是1段,16-31是一段,...
整數映射到256M段中? 如果整數是0-15,則增加第一段記數,如果整數是16-31,則增加第二段記數,...
其實可以不用分256M段,可以分的段數少一寫,這樣在掃描記數段時會快一些,還能節省一些內存)
分段計數,先找出中位數所在的數據區域,然后集中查找。具體算法如下:
1.整數int型,按照32位計算機來說,占4Byte,可以表示4G個不同的值。原始數據總共有10G個數,需要8Byte才能保證能夠完全計數。而內存是2G,所以共分成2G/8Byte=250M個不同的組,每組統計4G/250M=16個相鄰數的個數。也就是構造一個雙字數組(即每一個元素占8Byte)統計計數,數組包含250M個元素,總共占空間8Byte*250M=2G,恰好等於內存2G,即可以全部讀入內存。第一個元素統計0-15區間中的數字出現的總個數,第二個元素統計16-31區間中的數字出現的總個數,最后一個元素統計(4G-16)到(4G-1)區間中的數字出現的總個數,這樣遍歷一遍10G的原始數據,得到這個數組值。
2.定義一個變量sum,初始化為0。從數組第一個元素開始遍歷,並把元素值加入到sum。如果加入某個元素的值之前,sum<5G;而加入這個元素的值之后,sum>5G,則說明中位數位於這個元素所對應統計的16個相鄰的數之中,並記錄下加入這個元素的值之前的sum值(此時sum是小於5G的最大值)。如果這個元素是數組中第m個元素(m從0開始計算),則對應的這個區間就是[16m,16m+15]。
3.再次定義一個雙字數組統計計數,數組包含16個元素,分別統計(16m)到(16m+15)區間中的每一個數字出現的個數,其他數字忽略。這樣再次遍歷一遍10G的原始數據,得到這個數組值。
4.定義一個變量sum2,sum2的初始值是sum(即上述第二步中記錄的小於5G的最大值)。從新數組第一個元素開始遍歷,並把元素值加入到sum2。如果加入某個元素的值之前,sum2<5G;而加入這個元素的值之后,sum2>5G,則說明中位數就是這個元素所對應的數字。如果這個元素是新數組中的第n個元素(n從0開始計算),則對應的數字就是16m+n,這就是這10G個數字中的中位數。
算法過程如上,需要遍歷2遍原始數據,即O(2N),還需要遍歷前后2個數組,O(k).總時間復雜度O(2N+k)
只有2G內存的pc機,在一個存有10G個整數的文件,從中找到中位數,寫一個算法。
那么根據N0和N1的大小就可以知道中位數的最高位是0還是1
假設N0>N1,那么再計算N00和N01,
如果N00>(N01+N1),則說明中位數的最高兩位是00
再計算N000和N001.。。。依次計算就能找到中位數
好像一次磁盤io也可以統計出N0,N00,....的數值
一個整數假設是32位無符號數
第一次掃描把0~2^32-1分成2^16個區間,記錄每個區間的整數數目
找出中位數具體所在區間65536*i~65536*(i+1)-1
第二次掃描則可找出具體中位數數值
第一次掃描已經找出中位數具體所在區間65536*i~65536*(i+1)-1
然后第二次掃描再統計在該區間內每個數出現的次數,就可以了
