0. 前言
该文章会长期收录一些关于 海量数据处理 的常见问题,在面试中很容易被问到,希望做以记录帮助到读者。
1. 位图应用
1. 给定 100亿 个整数,设计算法找到只出现一次的整数?
- 方法一
- 遇到海量数据问题,首先得分析数据大小。如果我们需要存放一个
int
范围的所有数据,即4G
个数据,我们仅需要4G / 8 = 512M
大小的空间就够了。 - 根据位图的性质,我们采用一个位图是无法得到只出现一个的整数的。在此很容易想到 双位图 的做法:
- 用位图 1 来确定数字是否出现,若出现对应位置置 1
- 用位图 2 来确定数字是否再次出现,当位图 1 的对应位置已经为 1 时,且该数字再次出现,即在位图 2 进行置 1
- 这样便能够查找得到只出现一个的整数,所用两个
512M
的位图,那么就是额外1G
的空间
- 方法二
- 这个问题也可以用一个大位图来进行解决,以前的
512M
的位图表现了int
型数据的所有情况,但只能表现出现过,并不能表现多次出现。究其原因还是只用了 1 个比特位来表示,那么就只能产生 0 、1 这两种情况,在此我们采用 2 个比特位表示一个数字的出现情况,这样就能得到 4 中情况,出现0、1、2、3次,也就是两张普通位图的合并结果,这样做也是1G
的空间。参照我们普通位图的实现 [C++ 系列] 84. 位图的概念及应用 稍加改造就能解决该问题。
参见代码如下:
#include<stdio.h>
#include<assert.h>
#include<windows.h>
typedef struct TwoBitSet
{
// 数组
size_t *bts;
// 数据的范围
size_t range;
}TwoBitSet;
void TBSInit(TwoBitSet *tbs,size_t range)
{
assert(tbs);
// 需要两位表示一个数据,所以除以16,
// (range >> 4) + 1计算需要多少个整型
tbs->bts = (size_t *)malloc(((range >> 4) + 1) * sizeof(size_t));
assert(tbs->bts);
memset(tbs->bts, 0,((range >> 4) + 1) * sizeof(size_t));
tbs->range = range;
}
int TBSGetValue(TwoBitSet *tbs, size_t x)//
{
assert(tbs);
// 计算x在数组里下标
size_t index = x >> 4;
// 因为两位表示一位,需要乘2
size_t num = x % 16 * 2;
// 不能改变tbs->bts[index],所以设置value
int value = tbs->bts[index];
// 将value向右移
value >>= num;
// 任何数和3(11)&为任何数
return value & 3;
}
void TBSSetVaule(TwoBitSet *tbs, size_t x, size_t value)//设置值
{
assert(tbs);
// 计算x在数组里下标
size_t index = x >> 4;
// 因为两位表示一位,需要乘2
size_t num = x % 16 * 2;
if (value == 0)//设置为00
{
// 先将3左移到要设置的位,然后取反,保证在这两位为0,
// 然后&,这两位为0,其他为不变
tbs->bts[index] &= ~(3 << num);
}
// 要设置为01
else if (value == 1)
{
// 设置1
tbs->bts[index] |= (1 << num);
tbs->bts[index] &= ~(1 << (num + 1));
}
// 要设置为10
else if (value == 2)
{
// 设置1
tbs->bts[index] |= (1 << (num + 1));
// 设置0
tbs->bts[index] &= ~(1 << num);
}
// 要设置为11
else if (value == 3)
{
tbs->bts[index] |= (3 << num);
}
}
// 销毁
void BTSDestory(TwoBitSet *tbs)
{
assert(tbs);
free(tbs->bts);
tbs->bts = NULL;
tbs->range = 0;
}
2. 给两个文件,分别有 100 亿个整数,我们只有 1G
内存,如何找到两个文件交集?
- 如问题 1:一个文件
512M
,文件A
中数据出现一次将位图 1 中该位设置为 1,再出现不用设置 - 文件
B
中数据出现一次将位图 2 中该位设置为 1,再出现不用设置(一个整型 32 位,存 32 个数据),然后位图1&
位图 2,&
结果为 1 的位则是两个文件的交集。
- 当所给的内存再小的话, 例如仅给
512M
的内存,那我们就需要对该文件进行文件切分。具体操作就是:设置一种 / 多种哈希规则,将大文件进行切分成小的文件,那么相同的数字在同种哈希映射下会出现在相同的小文件中。然后分别对各个小文件进行找交集即可。主要考察文件分割。
3. 位图应用变形:1 个文件有 100 亿个 int
,1G
内存,设计算法找到出现次数不超过 2 次的所有整数
- 该问题解法同问题 1 的解法。两张位图两位表现 4 种情况其中 00 出现 0 次,01 出现 1 次,10 出现 2 次, 11 出现 2 次以上。扫描 01、10 这两种情况就能够解决该问题了。
2. 布隆过滤器应用
1. 给两个文件,分别有 100亿 个 query
,我们只有 1G
内存,如何找到两个文件交集?分别给出精确算法和近似算法
- 精确算法参考位图应用问题 2 即可
- 近似算法就采用布隆过滤器即可,将第一个文件的所有内容放进布隆过滤器,再对第二个文件的所有内容在布隆过滤器中查找其是否存在。则为一种近似的求交集算法。
2. 如何扩展布隆过滤器使得它支持删除元素的操作
- 需要对布隆过滤器进行简单改造,将每一位改造为一个计数器,一旦用到该位置不再执行置 1 的操作,而是将该计数器
++
,当删除数据时也不再是置 0,而是将计数器--
,当位置为 0 时,说明该数据不存在。但是这个计数器到底设置多大呢?这得考虑到数据的大小,若设置为int
将有32
来表示数据存在与否,但是这样完全丧失了布隆过滤器的优点。若设置为char
这样会很容易产生计数回绕,因为它只能保存 256 种情况。
3. 哈希切割
1. 给一个超过 100G
大小的 log file
,log
中存着 IP
地址, 设计算法找到出现次数最多的 IP
地址? 与上题条件相同,如何找到top K的IP?如何直接用Linux系统命令实现?
IP
地址是一个点分十进制,unsigned_int
,最简单粗暴的方法就是直接进行排序,设置4G
内存对所有的IP
地址进行标志,也就是拿4G * 4 = 16G
个空间进行处理。在服务器上16G
并不是什么难事情。但是效率就很低下了。- 采用哈希切割的方式,设置哈希函数,对文件进行拆分,那么相同的数据通过同样的哈希函数肯定存放在相同的小文件中。能知道一个
IP
地址是一个 4 字节的整数,我们可以通过后 2 字节有2^16
种可能性,将其分为 65536 个桶,然后 65536 个桶中会存在 65536 种可能性,这个可能性来自前 2 个字节。然后可以在 65536 个桶中查找到其桶内的出现次数最多的IP
,然后再将这些进行再排序,就可以得到第一名。同理得到TOP K
Linux
首先使用sort
指令排序log_file
,这样就可以得到一个升序排序,此时相同的IP
将会相邻。再采用uniq
指令去重,这里需要uniq -c
将会将合并了多少行直接显示在前面,再使用sort -nr
其中sort -n
会对数字进行排序,即对合并后的行号大小进行排序。sort -nr
即对合并后行号进行降序排序。再采用head - K
即可得到前K
重复IP
。综合就是:sort log_file | uniq -c | sort -nr | head -k
4. 倒排索引
1. 给上千个文件,每个文件大小为 1K—100M
。给 n
个词,设计算法对每个词找到所有包含它的文件,你只有 100K
内存
- 简单理解下倒排索引:在文件中找对应单词,那么就是正序索引。那么拿着这
n
个词找对应文件就是倒排索引。扫描所有文件,若该单词在某文件出现的话就在单词后面记录下文件号即可。这样扫描完毕所有的文件就能够解决这个问题。至于这100M
的文件仅有100K
的内存,只需要对文件进行切割就好了。
5. 后续补充
Trie树
即哈希多叉树,字典树,单词查找树,前缀树,其用于在大量字符串中快速检索。
跳跃链表、跳跃表、跳表等相关海量数据问题。