面試常見-海量數據處理

轉載 + 修改
1. 如何從大量的 URL 中找出相同的 URL?

  • 給定a、b兩個文件,各存放50億個URL,每個URL各佔64B,內存限制是4GB,請找出a、b兩個文件共同的URL
    方法總結:哈希取餘,分而治之,哈希查重
  • 50×109×64÷210320GB50 \times 10^9 \times 64 \div 2^10 \approx 320 GB
  • 320GB÷1000300MB320GB \div 1000 \approx 300MB
  • 所以:
  • hash(URL)%1000300MBhash(URL) \%1000 \approx 300MB
  • 根據計算結果把 fileafile_a 中的 URL 放在 filea0,filea1,...,filea999file_{a_0}, file_{a_1}, ..., file_{a_{999}} 中;
  • filebfile_b 中的 URL 執行相同的操作之後存放在 fileb0,fileb1,...,fileb999file_{b_0}, file_{b_1}, ..., file_{b_{999}} 中;
  • 這樣處理之後,所有有可能是相同 URL 的必定在同個 filea/bnfile_{{a/b}_{n}} 中;
  • 即, 接下來,我們只要對比 fileanfile_{a_n} 以及 filebnfile_{b_n} 中的 URL 即可;
  • 先把 fileanfile_{a_n} 存放進一個 hashmap 中, 再遍歷 filebnfile_{b_n} 中的元素,若能在 hashmap.find() 能找到,就說明是重複的 URL,則將重複的 URL 存進 fileansfile_{ans} 中。

2. 如何從大量數據中找出高頻詞?

  • 有一個 1GB 大小的文件,文件裏每一行是一個詞,每個詞的大小不超過 16B,內存大小限制是 1MB,要求返回頻數最高的 100 個詞(Top 100)。
    分治策略,哈希統計詞頻,小頂堆找Top100
  • 1GB%5000200KB<1MB1GB \% 5000 \approx 200KB < 1MB, 對 5000 取餘,所以可以得到 5000 個小文件:file1,file2,...file4999file_1, file_2, ... file_{4999}
  • 單獨處理每一個小文件,可以存放在 hashmap 裏,key 是 word,value 是個數;
  • 存放進去之前先檢查是否在 hashmap 裏,如果在,則 ptr->second++,否則,插入 make_pair(word, 1)
  • 接下來,我們可以維護一個小頂堆來找出出現頻率最高的 100 個;
  • 構建一個小頂堆,堆的大小爲 100
  • 如果遍歷到詞頻大於隊堆頂的出現次數,則用新詞代替堆頂,然後重新調整爲小頂堆;
  • 遍歷結束之後,小頂堆上面的詞就是 TOP100TOP100 的詞。

如何找出某一天訪問百度網站最多的 IP?

  • 現有海量日誌數據保存在一個超大文件中,該文件無法直接讀入內存,要求從中提取某天訪問百度次數最多的那個 IP。
    分治策略,哈希分成小文件,MAX 記錄 Top1
  • hash(ip)%mhash(ip) \% m 得到 m 個小文件;
  • 再對每個小文件,遍歷存進 hashmap 中,key 爲 ip,value 爲頻率,且同時使用一個 max 保存最大的 ptr->second, 以及 s = ptr->first
  • 全部遍歷結束之後, s 即爲我們要找的 ip。

如何在大量的數據中找出不重複的整數?

  • 在 2.5 億個整數中找出不重複的整數。注意:內存不足以容納這 2.5 億個整數。
    位圖法,大量節省內存
  • 位圖:用 1 個或者多個 bit 位來表示對應的整數。
  • 這道題要求不重複,即每個數至少有三個狀態:
  • 位爲00:該數不存在;
  • 位爲01:該數存在一次;
  • 位爲10:該數至少存在兩次;
  • 將海量數據分爲小於計算機內存的小文件,遍歷每個小文件;
  • 再設置該數對應的位,原先是00則改爲01,原先是01則改爲11;
  • 最後遍歷 bitMap,爲11的即爲重複的數據。

如何在大量的數據中判斷一個數是否存在?
做法一:分治法 + 哈希
做法二:位圖法

#include <iostream>
#include <bitset>
using namespace std;
const int maxNum = 10000000;
int main() {
    int n, t;
    bitset<maxNum> bitMap(0);
    cin >> n;
    for(int i = 0; i < n; i++){
        cin >> t;
        bitMap.set(t, 1);
    }
    for(int i = 0; i < maxNum; i++)
        if(bitMap[i]) cout << i << " ";
    cout << endl;
    return 0;
}

如何查詢最熱門的查詢串?

  • 搜索引擎會通過日誌文件把用戶每次檢索使用的所有查詢串都記錄下來,每個查詢串的長度不超過 255 字節。假設目前有 1000w 個記錄(這些查詢串的重複度比較高,雖然總數是 1000w,但如果除去重複後,則不超過 300w 個)。請統計最熱門的 10 個查詢串,要求使用的內存不能超過 1G。(一個查詢串的重複度越高,說明查詢它的用戶越多,也就越熱門。)
  • 255B×1072.55GB255B \times 10^7 \approx 2.55GB,所以一次性裝不下內存;
  • 雖然字符串總數比較多,但去重後不超過 300w,因此,可以考慮把所有字符串及出現次數保存在一個 HashMap 中,所佔用的空間爲 300w×(255+4)777MB300w\times(255+4)\approx777MB(其中,4表示整數佔用的4個字節)。由此可見,1G 的內存空間完全夠用;
    方法一:遍歷一次存進哈希表,再維護一個大小爲 10 的最大堆
  • 首先遍歷字符串,若不在 Map 中,則插入 make_pair(srtring, 1)
  • 若在 Map 中,則 ptr->second++
  • 這一步時間複雜度爲 O(n)\mathcal{O}(n)
  • 之後遍歷Map,同時構建並維護一個大小爲 10 的最大堆;
  • ptr->second > maxHeap.top(),則進行替換;
  • 遍歷 Map 結束後,maxHeap 中即爲最熱門的 10 個查詢串;
  • 這一步的時間複雜度是:O(nlogn\mathcal{O}(n\cdot log_n)
    方法二:前綴樹
  • 當這些字符串有大量相同前綴時,可以考慮使用前綴樹來統計字符串出現的次數,樹的結點保存字符串出現次數,0 表示沒有出現。
  • 在遍歷字符串時,在前綴樹中查找,如果找到,則把結點中保存的字符串次數加 1,否則爲這個字符串構建新結點,構建完成後把葉子結點中字符串的出現次數置爲 1。
  • 最後依然使用大頂堆來對字符串的出現次數進行排序。
    前綴樹經常被用來統計字符串的出現次數。它的另外一個大的用途是字符串查找,判斷是否有重複的字符串等。

如何統計不同電話號碼的個數?

  • 已知某個文件內包含一些電話號碼,每個號碼爲 8 位數字,統計不同號碼的個數。
    本質還是海量數據的查重,位圖法
  • 8 位電話號碼可以表示的號碼個數爲 10810^8 個,即 1 億個。我們每個號碼用一個 bit 來表示,則總共需要 1 億個 bit,內存佔用約 100M。
  • 申請一個長度爲 1 億的位圖,初始化爲 0;
  • 然後遍歷所有的電話號碼;
  • 再將號碼對應位圖中的位置設置爲1;
  • 之後遍歷 bitMap,統計位爲 1 的個數,即爲不同號碼的個數。

如何從 5 億個數中找出中位數?

  • 從 5 億個數中找出中位數。數據排序後,位置在最中間的數就是中位數。當樣本數爲奇數時,中位數爲 第 (N+1)/2 個數;當樣本數爲偶數時,中位數爲 第 N/2 個數與第 1+N/2 個數的均值。
    分治法:按照高位是否爲 1 劃分爲小文件
  • 依次讀取 5 億個數據,對於每一個 Num,判斷其二進制的最高位是否爲 1 還是 0,將 Num 放進 file0file_0 或者 file1file_1 中。
  • 如果 file0file_0 的個數大於 file1file_1 的個數,則中位數一定在 file0file_0 中;
  • 用同樣的方法,根據次高位是否爲 1 劃分成 file01file_{01}file01file_{01}
  • 重複上述步驟直至文件可以加載進內存;
  • 加載進內存之後,將其排序,求出中位數。
  • 如果在某一步分到的 fileleftfile_{left}filerightfile_{right},如果劃分後兩個文件中的數據有相同個數,那麼中位數就是數據較小的文件中的最大值與數據較大的文件中的最小值的平均值。

如何按照 query 的頻度排序?

  • 有 10 個文件,每個文件大小爲 1G,每個文件的每一行存放的都是用戶的 query,每個文件的 query 都可能重複。要求按照 query 的頻度排序。
    方法一:重複率高,直接哈希
  • 如果 query 重複率高,說明不同 query 總數比較小,可以考慮把所有的 query 都加載到內存中的 HashMap 中。接着就可以按照 query 出現的次數進行排序。
    重複率低,選擇分治+哈希
  • 分治法需要根據數據量大小以及可用內存的大小來確定問題劃分的規模。對於這道題,可以順序遍歷 10 個文件中的 query,通過 Hash 函數 hash(query) % 10 把這些 query 劃分到 10 個小文件中。之後對每個小文件使用 HashMap 統計 query 出現次數,根據次數排序並寫入到零外一個單獨文件中,接着對所有文件按照 query 的次數進行排序,這裏可以使用歸併排序(由於無法把所有 query 都讀入內存,因此需要使用外排序)。

如何找出排名前 500 的數

  • 有 20 個數組,每個數組有 500 個元素,並且有序排列。如何在這 20*500 個數中找出前 500 的數?假設元素降序排列。
  • 首先建立一個大小爲 20 的大頂堆,存放每個數組的 Top 1,即存放每組最大的值;
  • 接着刪除堆頂元素 num,並將其存放進目標數組中;
  • 向大頂堆中插入 num 原先坐在所在數組的下一個元素;
  • 如此循環 500 次。

Top K 問題
情景一:數據量小,內存可以裝得下,則一邊輸入一邊維護一個最小堆
情景二:數據量大,內存一次性裝不下,先分爲數據量大小合適的小文件

#include <iostream>
#include <queue>
#include <vector>
using namespace std;

// 由於 STL 自帶優先隊列是默認最大優先的,
// 所以自己寫了一個比較函數,將其改爲最小優先
struct cmp1 {
    bool operator ()(const int &a, const int &b) {
        return a < b;
    }
};

int main() {
    int n, k, tmp;
    while(cin >> n >> k) {
        if(k > n) return 0;
        priority_queue<int, vector<int>, cmp1> q;
        for(int i = 0; i < k; i++) {
            cin >> tmp;
            q.push(tmp);
        }
        for(int i = k; i < n; i++) {
            cin >> tmp;
            if(tmp < q.top()) {
                q.pop();
                q.push(tmp);
            }
        }
        while(!q.empty()) {
            cout << q.top() << " ";
            q.pop();
        }
        cout << endl;
    }
    return 0;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章