Top K問題想必面試過的盆友都遇到過,比如億萬級資料如何查詢前十的請求/IP地址等的Top K問題,還有類似億萬級資料如何在資料庫中分頁
今天在這裡總結下,直接上乾貨不多BB,希望大家理解後可以在面試中有更好的表現
1 Top K
題目:大資料量請求,找出請求次數最多的IP地址
思路:分治+Hash
詳解:
1)按照每天為單位處理,IP地址有2^32=4G鍾取值,所以肯定不能把一天的資料量全部讀在記憶體中,我們假設這裡的資料檔案為4G的情況(根據實際業務場景變更資料大小即可,演算法是不變的)
2)使用分治思想,把資料分為1024個小檔案,IP地址經過Hash函式處理取模%1024,把大檔案分成1024個小檔案,這樣每個檔案只有4MB大小。
3)雜湊法(掃描兩遍,時間複雜度為O(N)),先對每個小檔案建立HashTable(我用的是Python,所以建立字典,如果想字典有序可以使用collctions中的Orderdict資料結構),Key為IP地址,Value為出現的次數(初始為0),掃描一遍先建出來HashTable,然後再掃一遍記錄每個IP地址出現的次數。
4)再根據每個檔案的HashTable中的Ip地址進行排序,排序指標為出現的次數也就是Value。快排和堆排都可以,個人比較喜歡回答堆排序的思想。如果結果是Top10的情況建立一個大小為10的小根堆,堆頂就是最小的IP地址對應的出現次數,建堆的時間複雜度為O(logN),然後遍歷檔案中的IP地址的Value。如果大於堆頂(堆頂為最小的出現次數),那麼堆頂與當前數值互換,進行堆調整,調整的時間複雜度為O(logN)。最後的小根堆,所得及所求。
2 海量字串最高的頻率查詢(Top K變種)
題目:一個G的檔案,每一行是一個詞,詞大小不超過16k,記憶體限制為1M,返回頻率最高的100個詞
詳解: 1)順序讀取檔案中的每行,Python中可以使用with open方法,with open方法是按照上下文讀取檔案,並自動關閉檔案,由於Python中的讀取的流物件是一個可迭代物件所以可以使用For迴圈按照行讀取,放心再大的檔案都不會溢位。
2)把1G檔案分成5000份這樣每份大概200K,然後進行Hash取模,把對應行的內容存在對應的檔案中,如果其中檔案超過1M(雜湊衝突導致的),可以把該檔案繼續按照之前的方法分解為10份或者幾份直到滿足要求為止。
3)使用Tire樹做詞頻統計(使用HashMap也可以但是字首樹更適合做詞頻統計),在建立Tire樹時多加一個資料項,表示查詢的次數,那麼就可以表示以某個單詞的查詢次數。
4)對每個檔案建立最小堆,堆大小為100,詞對應的頻率存入檔案,這樣又得到至少五千個檔案,然後對檔案中的詞頻進行歸併排序
3 10個檔案每個檔案1G,放著使用者的QUERY,按照QUERY頻率排序
詳解: 典型的Top K問題
Plan A:
1)順序讀取10個檔案,存入10個檔案(雜湊取模%10),理論上雜湊函式會均分在10個檔案中,每個檔案大概為1G大小。
2)電腦記憶體為2G,建立HashTable,K-V鍵值對為QUERY-QUERY_TIMES,掃描一遍檔案count所有QUERY的出現次數。
3)最後根據Value:QUERY_TIMES進行排序,單個檔案使用隨機快排 ,堆排序常數項時間複雜度大於快排,並且在工程中定址時間也長,所以導致理論上大資料量堆排序快於快排,但是實際效果還是不如快排,排序好的資料存入檔案,這樣得到了10個獨立有序的檔案
4)對10個獨立有序的檔案進行歸併排序
Plan B:
1)一般來說QUERY只是重複的次數多,所以如果考慮一次性讀入記憶體中。
2)掃描一遍記憶體中的QUERY,然後建立Tire樹或者HashMap。Tire樹掃描一遍就可以獲得詞頻資料,而HashMap要掃兩邊,雖然都是O(N),但是HashMap是2N,Tire是N,然後存為檔案。
3)直接使用隨即快排
4 兩個檔案各存50億個url,每個url64個位元組,記憶體限制4G,找出A,B共同的url
1)單個檔案讀取肯定超出記憶體大小,所以還是採取之前的分治思想,大化小,對A/B分別取模分成1000個檔案儲存。50億url算下來每個檔案300M。
2)對小檔案求公共url的時候可以使用set去重。或者使用並查集的IsSameSet判斷,缺點是建立十個集合。A檔案Set建立後另外一個檔案的內容遍歷跟Set中內容比對,如果相等則記錄
另:可以考慮使用BloomFilter,後面的黑名單問題中會詳解BloomFilter的
5 N億個數找出一個只出現一次的數
**詳解:**思路跟之前一樣,首先分成小檔案,然後建立HashTable/tire樹進行統計,差別還是Tire樹只需要掃一遍,HashTable需要掃兩遍
**另外:**可以使用BitMap,每個數分配兩Bit,00不存在,01出現一次,10出現多次,11沒意義。需要記憶體2^32*8bit=1G,建立完畢掃描資料把對應位置的位元位描成00/01/10/11,最後查詢01
6 有一百多萬個黑名單,如何判斷某個使用者是否被拉黑
BloomFilter最佳應用場景
BloomFilter介紹: 布隆過濾器屬於BitMap的變種。HashMap如果長度有限,資料量很大的情況下,採用拉鍊法解決雜湊衝突會導致鏈過長而查詢效能受到影響,採用地址開方法(Python中字典使用的解決Hash衝突的方法),那麼還是會導致空間不足。所以想節省空間Bitmap就誕生了。也就是說BloomFilter優點就是省空間,但是布隆過濾器的缺點就是會有失誤率。這個失誤率是指本來A不屬於黑名單,但是被誤判進了黑名單,寧可殺錯,絕不放過的一種失誤。
原理: 1.某個Key需要加入集合中,需要K個雜湊函式計算出K個hash值,並查詢對應的位元位,如果所有的位元位對應的值都是1,那麼就認為這個Key是在這個集合中的,相比HashMap不需要儲存Key節省空間
結構: 集合中每一個位置為位元,非0即1,一個Int型整數,4個位元組,32個位元,即一個1000的集合,可以表示32000個位元位,陣列的InteIndex表示陣列中的位置,BitIndex為陣列中某一位的位元。根據計算出來的hash值來描黑,最後由該位置的32Bit數或上左移的BitIndex位的1實現
JavaCode
應用到案例: 黑名單問題,url經過k個hash函式進行處理後模個m,對應0~m-1上的一個,算出位置描黑,有可能不同url打在同一位置,重複描黑。如果k個位置都是黑的那麼,這個url就在黑名單裡
失誤率控制
通過控制m長度和k來控制失誤率
確定陣列長度M M=-(n*lnp)/(ln)^2,n是樣本量,p預期失誤率,算出來是bit,對應的實際位元組數應該除以8
附加題
題目:千萬級資料分頁如何實現?
思考:海量資料分頁已經無法使用資料庫內的分頁方法LIMIT,因為這樣會嚴重影響效能。常見的解決方法有兩種:1.將查詢資料讀入記憶體中,然後在記憶體中分頁,限制每次的查詢量 2.採用儲存過程分頁,不同資料庫實現方式不同,並且效能達不到預期
優解:
1)在待查詢的表中新增一個自增長欄位,Query_ID,主鍵自增長。按照大小順序逆序排列資料。
語句: SELECT QUERY_ID FORM TABLE WHERE XXX ORDER BY DESC
因為只是查詢QUERY_ID欄位所以,即使資料量很大查詢速度也是很快的,然後將查詢到的QUERY_ID儲存應用伺服器的一個陣列中
2)使用者在客戶端進行翻頁的時候,客戶端將查詢的頁號作為引數傳遞給應用伺服器。伺服器通過頁號和QUERY_ID陣列計算出待查詢的最大值和最小值,然後查詢。
計算QUERY_ID最大最小值: 定義page:待查詢的頁號;pagesize為每頁的大小;query_ids為之前生成的陣列
優點:
1)所有資料庫都適用
2)CPU和記憶體佔用較低
3)查詢速度較快
物理優化:
1)資料分割槽
2)增加記憶體
3)資料分表
還可以分成兩個庫,一個做查詢,一個做事務
....三天終於寫完了