面试常见-海量数据处理

转载 + 修改
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;
}

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