1. 程式人生 > >面試經典的海量資料處理(TOPK)問題—轉載+個人見解!

面試經典的海量資料處理(TOPK)問題—轉載+個人見解!

常見問題:

Top K問題分治+Trie樹/Hash_map+小頂堆。採用Hash(x)%M將原檔案分割成小檔案,如果小檔案太大則繼續Hash分割,直至可以放入記憶體。

重複問題BitMap點陣圖 或 Bloom Filter布隆過濾器 或 Hash_set集合。每個元素對應一個bit處理。

排序問題外排序 或 BitMap點陣圖。分割檔案+檔案內排序+檔案之間歸併。

Top K問題:

1. 有一個1G大小的一個檔案,裡面每一行是一個詞,詞的大小不超過16位元組,記憶體限制大小是1M。返回頻數最高的100個詞。

①分治:順序讀檔案,對每個詞x取Hash(x)%2000,按照該值存到2000個小檔案中。每個檔案是500k左右。如果有檔案超過了1M則繼續分割。O(N)

②Trie樹/Hash_map:字串用Trie樹最好。對每個小檔案,統計其中出現的詞頻。O(N)*(平均字元長度),長度一般是常數,也就是O(N). 

③小頂堆:用容量為100的小頂堆,以頻率為value值插入,取每個檔案現頻率最大的100個詞,把這100個詞及相應的頻率存入檔案。最差O(N)*lg(100),也就是O(N).注:2,3步驟合起來需要一輪磁碟存取過程。存入檔案的個數可以縮減一下,因為主要開銷在磁碟讀取上,減少檔案讀取次數,可以在每個檔案存取最大容量的字元數量,比如這道題1*(M/16位元組字串長度+頻率(int)8位元組)的數存到一個檔案中。比如20000個詞存在一個檔案中,可以縮減到10個檔案。這樣最後一步只需要讀取10次就可以了。

④歸併:將得到的10個檔案裡面的數進行歸併,取前100個詞。注:我覺得其實不需要多路歸併,因為只需要找top100的數,歸併排序首先是nlgn的複雜度,第二是頻繁的磁碟存取,這裡最好是還是在記憶體建立容量為100的小頂堆,依次讀檔案,遍歷每個檔案中的元素更新小頂堆,這樣只需10次存取,並且時間複雜度是nlog100,也就是O(n)的。

註釋:為什麼說用Trie樹好,我之前一直沒想明白,因為網上說Trie樹是空間換時間,而這道題是空間敏感呀的。總結了一下,其實是兩點我沒想明白:

1.字串會通過一個hash演算法(BKDRHash,APHash,DJBHash,JSHash,RSHash,SDBMHash,可以自己看一下,基本就是按位來進行hash的

)對映為一個正整數然後對應到hash表中的一個位置,表中記錄的value值是次數,這樣統計次數只需要將字串hash一下找到對應位置把次數+1就行了。如果這樣的話hash中是不是不用儲存字串本身?如果不儲存字串本身,那應該是比較省空間的。而且效率的話因為Tire樹找到一個字串也是要按位置比較一遍,所以效率差不多呀。但是,其實字串的hash是要儲存字串本身的,不管是開放地址法還是散列表法,都無法做到不衝突。除非桶個數是字串的所有情況26^16,那是肯定空間不夠的,因此hash表中必須存著字串的值,也就是key值。字串本身,那麼hash在空間上肯定是定比不過Trie樹的,因為Trie樹對公共字首只儲存一次。

2.為什麼說Trie樹是空間換時間呢,我覺得網上這麼說不甚合理,這句話其實是相對於二叉查詢樹來說的,之所以效率高,是因為二叉查詢樹每次查詢都要比較大小,並且因為度為2,查詢深度很大,比較次數也多,因此效率差。而Trie樹是按位進行hash的,比如26個字母組成的字串,每次找對應位的字元-‘a’就是位置了。而且度是26,查詢深度就是字串位數,查詢起來效率自然就很快。但是為啥說是空間換時間,是因為字串的Trie樹若想儲存所有的可能字串,比如16位,一個點要對應下一位26種情況,也就是26個分支,也得26^16個位置,所以空間是很大的。但是Trie樹的話可以採用依次插入的,不需要每個點記錄26個點,而是隻存在有值的分支,Trie樹節點只要存頻率次數,插入的流程就是挨個位子找分支,沒有就新建,有就次數+1就行了。因此空間上很省,因為重複字首就統計一次,而效率很高,O(length)。

2. 海量日誌資料,提取出某日訪問百度次數最多的那個IP。注:跟上一題一致,甚至更簡單,不需要考慮trie樹。

①分治:IP是32位,共有232個IP。訪問該日的日誌,將IP取出來,採用Hash,比如模1000,把所有IP存入1000個小檔案。

②Hash_map:統計每個小檔案中出現頻率最大的IP,記錄其頻率。

③小頂堆:這裡用一個變數即可。在這1000個小檔案各自最大頻率的IP中,然後直接找出頻率最大的IP。

3. 海量資料分佈在100臺電腦中,想個辦法高效統計出這批資料的TOP10。注:主要不同點在於分散式

分析:雖然資料已經是分佈的,但是如果直接求各自的Top10然後合併的話,可能忽略一種情況,即有一個數據在每臺機器的頻率都是第11,但是總數可能屬於Top10。所以應該先把100臺機器中相同的資料整合到相同的機器,然後再求各自的Top10併合並。

①分治:順序讀每臺機器上的資料,按照Hash(x)%100重新分佈到100臺機器內。接下來變成了單機的topk問題。單臺機器內的檔案如果太大,可以繼續Hash分割成小檔案。

②Hash_map:統計每臺機器上資料的頻率。

③小頂堆:採用容量為10的小頂堆,統計每臺機器上的Top10。然後把這100臺機器上的TOP10組合起來,共1000個數據,再用小頂堆求出TOP10。

4. 一個文字檔案,大約有一萬行,每行一個詞,要求統計出其中最頻繁出現的前10個詞,請給出思想,給出時間複雜度分析。 注:檔案大小不需要分割檔案

①分治:一萬行不算多,不用分割檔案。

②Trie樹:統計每個詞出現的次數,時間複雜度是O(n*le)  (le表示單詞的平準長度)

③小頂堆:容量為10的小頂堆,找出詞頻最多的前10個詞,時間複雜度是O(n*lg10)  (lg10表示堆的高度)

總的時間複雜度是 O(n*le)與O(n*lg10)中較大的那一個。

5. 一個文字檔案,找出前10個經常出現的詞,但這次檔案比較長,說是上億行或十億行,總之無法一次讀入記憶體,問最優解。

比上一題多一次分割。分割成可以一次讀入記憶體的大小。

①分治:順序讀檔案,將檔案Hash分割成小檔案,求小檔案裡的詞頻。

②③同上。

6. 100w個數中找出最大的100個數。

方法1:用容量為100的小頂堆查詢。複雜度為O(100w * lg100)。小根堆是最好的方法。

方法2:採用快速排序的思想,每次分割之後只考慮比標兵值大的那一部分,直到大的部分在比100多且不能分割的時候,採用傳統排序演算法排序,取前100個。複雜度為O(100w*100)。

方法3:區域性淘汰法。取前100個元素並排序,然後依次掃描剩餘的元素,插入到排好序的序列中,並淘汰最小值。複雜度為O(100w * lg100)  (lg100為二分查詢的複雜度)

重複問題:

1. 給定a、b兩個檔案,各存放50億個url,每個url各佔64位元組,記憶體限制是4G,讓你找出a、b檔案共同的url?

分析:每個檔案的大小約為5G×64=320G,遠遠大於記憶體大小。考慮採取分而治之的方法。

方法1

①分治:遍歷檔案a,對每個url求Hash%1000,根據值將url分別儲存到1000個小檔案中,每個小檔案約為300M。檔案b採用同樣hash策略分到1000個小檔案中。上述兩組小檔案中,只有相同編號的小檔案才可能有相同元素。

②Hash_set:讀取a組中一個小檔案的url儲存到hash_set中,然後遍歷b組中相同編號小檔案的每個url,檢視是否在剛才構建的hash_set中。如果存在,則存到輸出檔案裡。

方法2

如果允許有一定的錯誤率,可以使用Bloom filter,使用位陣列,4G記憶體大概可以表示340億bit。將其中一個檔案中的url使用Bloom filter對映為這340億bit,然後挨個讀取另外一個檔案的url,檢查是否在Bloom filter中。如果是,那麼該url應該是共同的url(注意會有一定的錯誤率)。

注: bloom filter被用來檢測一個元素是不是集合中的一個成員。如果檢測結果為是,該元素不一定在集合中;但如果檢測結果為否,該元素一定不在集合中。主要思路是:將一個元素對映到一個 m 長度的陣列上,使用 k 個雜湊 函式對應 k 個點,如果所有點都是 1 的話,那麼元素在集合內,如果有 0 的話,元素則不在集合內。錯誤率:如何根據輸入元素個數n,確定位陣列m的大小及hash函式個數k,k=(ln2)*(m/n)時錯誤率最小,為f = (1 – e-kn/m)k 。

2. 在2.5億個整數中找出不重複的整數,記憶體不足以容納這2.5億個整數。

分析:2.5億個整數大概是954MB,也不是很大。當然可以更節省記憶體。整數一共2^32個數.每個數用2bit的話,需要1GB。也就是

方法1

採用2-Bitmap,每個數分配2bit,00表示不存在,01表示出現一次,10表示多次,11無意義。共需記憶體60MB左右。然後掃描這2.5億個整數,檢視Bitmap中相對應位,如果是00變01,01變10,10保持不變。所描完後,檢視Bitmap,把對應位是01的整數輸出。

注:感覺這個方法不對呀,bitmap要統計所有的整數值,2*3^32是需要1GB記憶體呀,不是60MB, 954MB都存不下怎麼存1GB?? 得到結論,bitmap統計整數存在性起碼得有1G的記憶體。也就是說少於268435456個數不如直接hash,消耗的記憶體反而更小!

方案2

分治法,Hash分割成小檔案處理。注意hash保證了每個檔案中的元素一定不會在其他檔案中存在。利用Hash_set,在小檔案中找出不重複的整數,再進行歸併。

方案3:

或者,我覺得可以將整個整數域劃的bitmap根據記憶體大小分成可以幾個檔案,比如劃分四個檔案,這樣的話0-1*2^30在一個範圍,,……,3*2^30-4*2^30在一個檔案中,記憶體只要保證250M大小即可。整數需要放在對應的bitmap裡面的對應位置,這裡位置使用的是相對偏移量(value-首元素大小)。跟方案2相比分割的 方法不一樣,以及每個小檔案可以使用bitmap方法,所以更快一些。只是不知道有沒有這種分割。

3. 一個檔案包含40億個整數,找出不包含的一個整數。分別用1GB記憶體和10MB記憶體處理。

1GB記憶體: 

①Bitmap:對於32位的整數,共有232個,每個數對應一個bit,共需0.5GB記憶體。遍歷檔案,將每個數對應的bit位置1。最後查詢0bit位即可。

10MB記憶體: 10MB = 8 × 107bit

①分治:將所有整數分段,每1M個數對應一個小檔案,共4000個小檔案。注意計算機能表示的所有整數有4G個。

②Hash_set:對每個小檔案,遍歷並加入Hash_set,最後如果set的size小於1M,則有不存在的數。利用Bitmap查詢該數。

注:計算機能表示的整數個數一共有4G個,整數域hash分割成10M一個檔案,,一共分割成400個小檔案,每個小檔案判斷不存在的數,再把這些數全都歸併起來。磁碟IO次數越少越好!!所以不明白為啥1M對應一個小檔案,而不取最大的10M。

4. 有10億個URL,每個URL對應一個非常大的網頁,怎樣檢測重複的網頁?

分析:不同的URL可能對應相同的網頁,所以要對網頁求Hash。1G個URL+雜湊值,總量為幾十G,單機記憶體無法處理。

①分治:根據Hash%1000,將URL和網頁的雜湊值分割到1000個小檔案中,注意:重複的網頁必定在同一個小檔案中。

②Hash_set:順序讀取每個檔案,將Hash值加入集合,如果已存在則為重複網頁。 

排序問題:

1. 有10個檔案,每個檔案1G,每個檔案的每一行存放的都是使用者的query,每個檔案的query都可能重複。要求按照query的頻度排序。

方法1: 

①分治:順序讀10個檔案,按照Hash(query)%10的結果將query寫入到另外10個檔案。新生成的每個檔案大小為1G左右(假設hash函式是隨機的)。

②Hash_map:找一臺記憶體為2G左右的機器,用Hash_map(query, query_count)來統計次數。

③內排序:利用快速/堆/歸併排序,按照次數進行排序。將排序好的query和對應的query_count輸出到檔案中,得到10個排好序的檔案。

④多路歸併:將這10個檔案進行歸併排序。

方案2

一般query的總量是有限的,只是重複的次數比較多。對於所有的query,一次性就可能加入到記憶體。這樣就可以採用Trie樹/Hash_map等直接統計每個query出現的次數,然後按次數做快速/堆/歸併排序就可以了

方案3

與方案1類似,在做完Hash分割後,將多個檔案採用分散式的架構來處理(比如MapReduce),最後再進行合併。

2. 一共有N個機器,每個機器上有N個數。每個機器最多存O(N)個數並對它們操作。如何找到這N^2個數的中位數?

方法1: 32位的整數一共有232

①分治:把0到232-1的整數劃分成N段,每段包含232/N個整數。掃描每個機器上的N個數,把屬於第一段的數放到第一個機器上,屬於第二段的數放到第二個機器上,依此類推。 (如果有資料扎堆的現象,導致資料規模並未縮小,則繼續分割)

②找中位數的機器:依次統計每個機器上數的個數並累加,直到找到第k個機器,加上其次數則累加值大於或等於N2/2,不加則累加值小於N2/2。

③找中位數:設累加值為x,那麼中位數排在第k臺機器所有數中第N2/2-x位。對這臺機器的數排序,並找出第N2/2-x個數,即為所求的中位數。

複雜度是O(N2)。

方法2

①內排序:先對每臺機器上的數進行排序。

②多路歸併:將這N臺機器上的數歸併起來得到最終的排序。找到第N2/2個數即是中位數。

複雜度是O(N2*lgN)。