資源限制類問題的常用解決方案

Grey Zeng發表於2021-10-06

說明

以下提到的資料結構和元素型別均基於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。

更多

演算法和資料結構筆記

參考資料

程式設計師程式碼面試指南(第2版)

演算法和資料結構體系班-左程雲

相關文章