目录
1.1 给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?
2.2 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
2.3 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数?
3.1 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法
3.2 如何扩展BloomFilter使得它支持删除元素的操作
一、位图
我们将通过一个面试题来了解位图:
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在
这40亿个数中。【腾讯】
这个题有两个重点:一是数不重复,二是判断在不在,那么此题该怎么解呢?
遍历?排序+二分查找?还是哈希表,红黑树?这里一个很严重的问题就是数据量太大,要存储数据的话内存存不下,那么该如何解决呢?
这时用位图就很方便,我们需要开一个范围大小的比特位,一个无符号整数大小的范围,每一个整数映射一个比特位,就相当于直接定址法。无符号整数的范围是42亿9千多万,将其转换为空间,也就是说需要一个512M大小的空间即可。
一个数存不存在,我们到映射的那个比特位去看,如果为1即存在,如果为0即不存在。
1. 位图概念
所谓位图,就是用比特位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。
2. 位图的实现
通过位运算将映射的比特位更改成需要的状态,比如set,无论该比特位上的状态是0,还是1,都将该位改成1。那么怎么实现更改呢?range可以帮我们确定在哪个char的范围中,place可以帮我们确定具体位置,将1左移到映射的比特位下,再进行按位或运算。注:|(按位或) 两个比特位中有一个是1,结果为1。
reset是将映射的比特位的状态改为0,找到映射位置,先将1左移到映射的比特位下,再按位取反,此时除了映射的比特位下是0,其余位全是1,将取反后的值进行位运算。注:&(按位与):两个比特位中都是1,结果为1,任意一个为0,结果为0。
test是检查该值是否存在,存在返回真,不存在返回假。
template<size_t N>
class BitSet
{
public:
BitSet()
{ //因为除8后可能会丢掉后面的小数,所以+1,保证开足够的空间
_bs.resize(N / 8 + 1, 0);
}
//将映射位置的比特位改为1
void set(size_t x)
{
size_t range = x / 8;
size_t place = x % 8;
_bs[i] |= (1 << place);
}
//将映射位置的比特位改为0
void reset(size_t x)
{
size_t range = x / 8;
size_t place = x % 8;
_bs[i] &= (~(1 << place));
}
//检查映射位置的比特位是否为真
bool test(size_t x)
{
size_t range = x / 8;
size_t place = x % 8;
return _bs[i] & (1 << place);
}
private:
vector<char> _bs;
};
3. 位图的应用
1. 快速查找某个数据是否在一个集合中
2. 排序 + 去重
3. 求两个集合的交集、并集等
4. 操作系统中磁盘块标记
4. 位图的优缺点
优点:节省空间,查找存不存在速度快
缺点:只能针对整形,且一般要求范围集中,如果范围特别分散,空间消耗就会很大。
二、 布隆过滤器
位图可以很快速的帮我们找到某个数是否存不存在,但是它只能存放整形,如果我们要查找字符串怎么办呢?将哈希和位图结合就出现了布隆过滤器。
1. 布隆过滤器概念
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概
率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存
在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也
可以节省大量的内存空间。
如果只用一个哈希函数求映射值,由于字符串可以无限组合,所以肯定会有两个不同的字符串映射到同一个位置上,那么就会出现误判,什么是误判?一个字符串本来不存在过滤器中,由于它映射的那个位置别的字符串已经映射过了,所以在查找的时候会返回真,也就是这个字符串在过滤器中了,这就是误判。
为了降低误判的概率,我们可以把一个值通过不同的哈希函数映射在多个位置上,但是无论哈希函数再多也不会消除误判,而且函数函数增多以后,空间的消耗也会成倍数上升。
文章是关于各种哈希函数的讲解和效率对比,有兴趣可以看看:https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html
文章是布隆过滤器的原理,使用场景和注意事项,有兴趣的可以看看:详解布隆过滤器的原理,使用场景和注意事项 - 知乎
2. 布隆过滤器的实现
这里哈希函数我用三个,布隆过滤器开多少空间是通过一个公式算出来的,上面的文章里有详细说明。
插入就是将该值求出的三个映射位置的比特位都改为1。
查找这里也是找该值在位图中映射的三个是否都为1,如果都为1,则可能存在(因为会有误判),但是只要有一个映射位置不是1,那么该值一定不存在!
这里为什么没有删除?是因为删除一个值,要把它映射位置的比特位改为0,这样可能会影响其他值,所以没有实现删除函数。布隆过滤器官方也没有给具体的实现,如果需要的话就自己写一个吧,也不难。
struct BKDRHash
{
size_t operator()(const string& s)
{
// BKDR
size_t value = 0;
for (auto ch : s)
{
value *= 131;
value += ch;
}
return value;
}
};
struct DJBHash
{
size_t operator()(const string& s)
{
unsigned int hash = 5381;
for(auto& ch: s)
{
hash += (hash << 5) + ch;
}
return hash;
}
}; struct APHash
{
size_t operator()(const string& s)
{
unsigned int hash = 0;
int i = 0;
for (auto& ch :s)
{
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ ch ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));
}
++i;
}
return hash;
}
};
template<size_t N,class K = string,
class HashFunc1 = BKDRHash,
class HashFunc2 = APHash,
class HashFunc3 = DJBHash>
class BloomFilter
{
public:
void set(const K& key)
{
size_t hash1 = HashFunc1()(key) % (N * 5);
size_t hash2 = HashFunc2()(key) % (N * 5);
size_t hash3 = HashFunc3()(key) % (N * 5);
_bf.set(hash1);
_bf.set(hash2);
_bf.set(hash3);
}
bool test(const K& key)
{
size_t hash1 = HashFunc1()(key) % (N * 5);
if (!_bf.test(hash1))
return false;
size_t hash2 = HashFunc2()(key) % (N * 5);
if (!_bf.test(hash2))
return false;
size_t hash3 = HashFunc3()(key) % (N * 5);
if (!_bf.test(hash3))
return false;
return true; //存在但是会有误判
}
private:
std::bitset<N * 5> _bf;
};
布隆过滤器可以通过增加哈希函数的数量,或者改变布隆过滤器的长度,以此减小误判率。
完整代码:这里有当前布隆过滤器的误判率bitSet/bitSet/BloomFilter.h · 晚风不及你的笑/作业库 - 码云 - 开源中国 (gitee.com)
3. 布隆过滤器优缺点
优点:
- 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
- 哈希函数相互之间没有关系,方便硬件并行运算
- 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
- 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
- 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
- 使用同一组散列函数的布隆过滤器可以进行交、并、差运算
缺点:
- 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)
- 不能获取元素本身
- 一般情况下不能从布隆过滤器中删除元素
- 如果采用计数方式删除,可能会存在计数回绕问题
三、 海量数据面试题
1.哈希切割
1.1 给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?
100G大小的文件,这个我们直接用map,内存肯定放不下,那么怎么办呢?先用哈希切割将100G大文件切割成100个小文件,那么IP相同或者冲突的就都在一个文件里了。那么怎么分割呢?用一个仿函数把IP转化为整数,再进行取模得到映射位置,之后再将IP存入映射的文件中。将每个小文件依次读取到内存,用map去重后统计次数,读取完后再获取最多的那个IP就可以了。
这里为什么用哈希切割,不用平均分割呢?因为内存就那么大,大文件平均分割以后,相同的IP会出现在不同的文件里,此时统计次数就不准了,所以不能使用平均分割。
这里需要注意的是分割完成后,某个小文件的大小超过了1G怎么办?
此时还分两种情况处理:1.小文件里全是一个或多个IP重复,那么此时map是可以存放的,2.小文件里大多是不重复的IP,但冲突到这个小文件里了,此时map是存不下的。如果是第一种,我们可以直接用map存储,如果是第二种情况map存储不了,因为内存不够,会抛异常,我们可以将该小文件换一个哈希函数再次进行映射,将冲突的IP分到其他的文件去,文件变小了,问题就处理了。
2. 位图应用
2.1 给定100亿个整数,设计算法找到只出现一次的整数?
- 通过分析此题会有三种状态:1.出现0次,2.出现一次,3.出现2次及以上的。
- 虽然是100亿个整数,但是整数范围最大只有42亿9千多万,所以不用担心内存放不下的情况。而一个位图只有两种状态,那怎么办呢?我们可以开两个位图,如下图,上下组成一组,插入数据时可以分状态进行,如状态1:上下都为0,那么插入只改变bst2的比特位,如状态2,上0下1,那么将bst1里的比特位改为1,bst2里的比特位改为0,插入的时候是状态3则不需要改变,因为题目只要求找出现一次的,此时已经出现两次了,不符合要求了。
- 查找可以通过检查映射位置的状态进行判断,也可以通过遍历位图检查状态来判断。
2.2 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
这个题虽然只有1G内存,但是整数范围最大只有42亿9千多万,也就是要开512M大小的位图。因为有两个文件,所以要开两个位图,先插入进行去重,再通过遍历的方式查看两个位图的状态,都两个都为1,就有交集,也可以将两个位图进行按位与运算,完成后比特位上还是1的就是交集。
2.3 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数?
这个题是第一次的变形,题目要求找出不超过两次的整数,通过分析得出会出现4种状态:1.出现0次,2.出现1次,3.出现2次,4.出现3次及以上的。只需要插入和查找的时候多进行一次判断即可。
3. 布隆过滤器
3.1 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法
query一般是查询指令,比如是一个网络请求,或者是一个数据库sql语句。
精确算法:这里还是一样,用哈希切割的方式将A文件和B文件切割成若干个小文件,切割时要用同一个哈希函数,将映射关系相同的A小文件和B小文件分别插入不同的map中,去重后再找两个小文件的交集,然后换下一份小文件,直至结束。这里依旧会出现某个小文件特别大的情况,这时再换一个哈希函数进行映射,跟哈希切割那道题一样。
近似算法:因为文件很大,所以还需要先切割成小文件,再用布隆过滤器插入去重,然后找文件的交集,当然布隆过滤器会有误判。
3.2 如何扩展BloomFilter使得它支持删除元素的操作
将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。但是此方法还是有缺陷:1. 无法确认元素是否真正在布隆过滤器中,2. 存在计数回绕。