說明
以下提到的資料結構和元素型別均基於Java語言。
問題1
32位無符號整數的範圍是
0~4,294,967,295
(即:0 ~ 2^32 - 1
)現在有一個正好包含40億個無符號整數的檔案,可以使用最多1GB的記憶體,怎麼找到出現次數最多的數?
首先,需要考慮最差情況,假設40億個數都不一樣。
如果使用Hash表,key表示其中的某個數,value表示出現的次數,由於
40億 > Integer.MAX_VALUE
所以Hash表的key和value都需要long
型別來存,
Hash表中的每條記錄至少需要
8Byte + 8Byte = 16Byte
最差情況,40億條不同記錄進入Hash表,
需要
16Byte * 40億 = 640億Byte
約等於64G
。記憶體不夠。
如果申請陣列來存,由於Java中陣列長度有限制(Integer.MAX_VALUE
),所以要存下40億個數,一個陣列一定不夠。
long
型別的多個陣列假設把所有40億個不同的數都存下,
需要的記憶體空間是:
40億 * 8Byte = 320億Byte
約等於32G
, 記憶體也不夠。
如果只有1G
記憶體,如果用Hash表,key和value均為long
型別,即一條記錄大約8Byte
,保守估計,可以裝下
1G / 8 Byte = 10億Byte / 8Byte = 1.25億
,
在這個1.25億
基礎上再減少到1千萬
, 處理1千萬
種類的資料,絕對不會超過現有的記憶體限制。通過如下計算
40億 / 1千萬 = 400
我們可以建立400個空檔案,然後,對於每一個數m,通過hash函式得到一個hash值,假設m的雜湊值為n, 然後用n
hash(m) % 400 = n % 400 = i
得到的i的值是多少,就把m這個值分配到第i號檔案中。
根據hash函式的性質,相同的數一定進入同一個檔案。 且400個檔案中每個檔案大約都是1千萬種數。
使用hash表統計每個檔案中出現次數最多的數(最多1千萬種左右,hash表在現有資源限制下無壓力),就是整個檔案出現次數最多的數。
問題2
32位無符號整數的範圍是
0~4,294,967,295
,現在有一個正好包含40億個無符號整數的檔案,所以在整個範圍中必然存在沒出現過的數, 可以使用最多1GB的記憶體,怎麼找到所有未出現過的數?
如果使用HashSet,需要用long
型別來存,大約需要
40億 * 8Byte = 320億Byte
大約32G
,記憶體不夠。
我們可以使用bit陣列(點陣圖)來存每個數是否出現,0
表示出現,1
表示沒有出現。Java中,一個int
型別可以表示32
位, 因為要標識每個數是否出現過。所以2^32
個數大約需要
(2^32) / 8 = 537M
537M空間的記憶體佔用,滿足限制條件。
Java中,因為每個int
數字可以表示32位
的二進位制數,所以可以使用int
陣列來構造點陣圖,
參考如下示例程式碼(179這個數字是否出現過):
int[] arr = new int[10];
int i = 179;
int status = (arr[i / 32] & (i << (i % 32 ))) == 0 ? 0 : 1
status = 0
則表示沒有出現過,status = 1
則表示沒有出現過。
問題3
32位無符號整數的範圍是
0~4,294,967,295
,現在有一個正好包含40億個無符號整數的檔案,所以在整個範圍中必然存在沒出現過的數, 記憶體限制為3KB
,只用找到一個沒出現過的數即可。
3KB 如果用來表示long
型別的陣列,陣列最大長度大約是
3KB / 8Byte = 375
。大約375
長度, 然後我們尋找比375
小的離375
最近的2的某次方的數,得到256
, 那我們可以申請一個256
長度的long
型別陣列,假設叫arr
。
由於數字一共有2^32
次方個,我們可以將
2^32 / 256 = 16,777,216
均分成256份,每一份負責統計每16,777,216個數出現的次數。
arr[0] 統計 0 ~ 16,777,215
出現的次數。
arr[1] 統計 16,777,216 ~ (16,777,216 + 16,777,215)
出現的次數。
.....
arr[255] 統計 (2^32 - 1 - 16,777,215) ~ 2^32 - 1
出現過的數。
由於現在的數個數是40億, 不到2^32次方,所以,肯定有某個位置上的arr值不夠16,777,216
個。 由於只需要找到一個沒有出現過的數,所以只需要在不夠16,777,216這個範圍的位置上進行再一次的256
份的劃分,然後再次使用上述邏輯,直到劃分到某個數單獨作為一個範圍同時沒出現過,這個數就是我們需要找的數。
問題4
32位無符號整數的範圍是
0~4,294,967,295
,現在有一個正好包含40億個無符號整數的檔案,所以在整個範圍中必然存在沒出現過的數, 只能使用有限幾個變數,如何找到一個沒出現過的數(找到一個即可)。
參考問題3,我們可以設定一個變數L
定位第0個數,設定變數R
定位2^32-1
上的數,設定M
變數定位到中間位置。由於一共有2^32
次方個數,所以統計左邊和右邊都應該有2^31
個數,但是總共40億個數,所以必然有一邊不滿足2^31
方個數,然後不滿足的這一邊繼續二分,重複上述邏輯,即可找到沒有出現過的一個數字。
問題3和問題4類似,我們可以得到一個結論: 如果記憶體3KB,就用256分,如果是幾個變數,就用二分。
問題5
有一個包含100億個URL的大檔案,假設每個URL佔用64B,請找出其中所有重複的URL。
如果允許失誤率,這個問題可以使用布隆過濾器來解決。
如果不允許失誤,則可以使用問題1的方法,Hash函式結合取模操作,分到小檔案思想。
問題6
32位無符號整數的範圍是
0~4294967295
,現在有40億個無符號整數,可以使用最多1GB的記憶體,找出所有出現了兩次的數。
問題2中,我們拿一個bit來表示一個數出現過一次或者沒出現過,本問題我們可以拿兩個bit來表達一個數出現的次數。
00
表示沒出現
01
表示出現過1次
10
表示出現過2次
11
表示出現過3次及3次以上
問題7
32位無符號整數的範圍是
0~4294967295
,現在有40億個無符號整數, 可以使用最多3KB的記憶體,怎麼找到這40億個整數的中位數?
參考問題3的做法,把整個2^32
個數均分到一個256長度的long
型陣列中,每個位置管理16,777,216
個數出現的次數。 然後逐個累加區間中數字出現的次數,一直累加到21億左右,即可判斷,中位數一定存在這個區間中。
問題8
32位無符號整數的範圍是
0~4294967295
,有一個10G大小的檔案,每一行都裝著這種型別的數字,整個檔案是無序的,給你5G的記憶體空間,請你輸出一個10G大小的檔案,就是原檔案所有數字排序的結果。
我們定義一個資料結構
class Node {
long value;
long times;
}
value表示檔案中的數值,times表示檔案中的數值出現的次數。
然後設定一個大根堆存Node。value大的數值在堆頂, 假設堆大小我們設定為3,每次遍歷的數和次數加入大根堆。大根堆滿了以後,如果新加入一個大根堆中沒有的數,則比較新加入的數和大根堆堆頂元素,如果比堆頂元素小,則剔除堆頂元素,加入新元素。遍歷一輪以後,大根堆中的三個數一定是整個檔案中最小的三個數。然後把這三個數和次數依次寫入新檔案,然後繼續上述遍歷和處理。每次都可以拿到三個排序後的數值,直接加入到新檔案即可。
所以,因為本問題中有5G
記憶體,根據我們上述的方案,綽綽有餘。
問題9
某搜尋公司一天的使用者搜尋詞彙是海量的(百億資料量),請設計一種求出每天熱門Top100詞彙的可行辦法。
參考問題1,我們可以使用Hash函式結合取模操作,分到小檔案思想。
然後使用大根堆,統計每個檔案中的top100。
最後,把每個檔案對應的大根堆的堆頂元素彈出放入新的一個大根堆(假設這個大根堆叫superHeap)中。然後從這個新的大根堆的堆頂彈出堆頂元素,即為全域性top1。
然後看這個彈出的堆頂元素是來自於哪個檔案,繼續把這個檔案對應的大根堆堆頂元素彈出,繼續放入superHeap中,然後從superHeap彈出堆頂元素,就是全域性top2。
.....
依次類推,直到top100。