题目:
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断这个数是否在这40亿个数中。
思路:
把这40亿个数放进 unordered_set 里?显然不现实,因为光是40亿个整型就需要约15GB空间,更别提哈希表的其他资源消耗,内存空间不够。
注意到我们需要找的是一个数存不存在,记录一个数是否存在只需要一个bit位,这样的话,一个bit位一个地址,采用直接定址法只需要开 2 32 2^{32} 232 (整型能表示的数据个数) 个bit,也就是 512MB 空间
代码实现:
注意一些位运算的技巧
template<size_t N> // 非类型模板参数,表示要开的位数
class bit_set
{
public:
bit_set()
{
_bits.resize(N / 8 + 1, 0); // N/8会省略小数,导致开的空间不够,所以+1多开一个字节
}
// x位置设为1
void set(size_t x)
{
// 计算x位置在第几个字节
size_t i = x / 8;
// 计算在这一字节的第几个bit位
size_t j = x % 8;
_bits[i] |= 1 << j;
}
// x位置设为0
void reset(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_bits[i] &= ~(1 << j);
}
// 判断x在不在
bool test(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
return _bits[i] & (1 << j);
}
private:
vector<char> _bits;
};
其实库里面提供了位图容器
详情参考文档:bitset - C++ Reference (cplusplus.com)
位图应用
重写一个位图,让两个bit表示一个状态,一共三种状态:出现0次、出现1次、出现多次。
这样这个位图需要开的空间是原来位图的2倍,也就是1GB
代码实现:
使用两个bitset实现,就不需要重写映射关系了
template<size_t N>
class two_bitset
{
public:
//出现0次、1次、多次分别表示为:00、01、10
void set(size_t x)
{
bool in1 = _bs1.test(x);
bool in2 = _bs2.test(x);
if (!in1 && !in2) // 如果还未出现
{
// 设为01
_bs2.set(x);
}
else if (!in1 && in2) // 如果已经出现一次
{
// 设为10
_bs1.set(x);
_bs2.reset(x);
}
// 如果已经出现2次及以上,不动
}
bool isOnce(size_t x)
{
return !_bs1.test(x) && _bs2.test(x);
}
private:
bitset<N> _bs1;
bitset<N> _bs2;
};
给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件的交集?
分别建立两个位图,两个位图中都记为1的值就是交集
1个文件有100亿个整数,我们有1G内存,从中找出出现次数不超过2次的所有整数。
在第一题的基础上加一个状态,让 10 表示出现2次,11 表示出现3次即以上
位图优点:
节省空间,快
位图缺点:
只能针对整型。
位图只能针对整型,因为整型可以使用直接定址法,且不存在哈希冲突。如果将字符串转换成整型则可能出现哈希冲突,而位图中没有真正存入这个字符串,无法进行比对,所以不能解决哈希冲突的问题。
布隆过滤器则是对位图的一种改造,可以让一个 key 映射多个地址(使用多个哈希函数),降低哈希冲突的概率。不过它依然有一定的误判概率。在查找的时候,它只能告诉你这个值一定不在或可能在。
具体映射多少个地址,要看具体情况,映射的越多,哈希冲突的概率越小,但是占用的空间就越大。
如何选择合适的哈希函数个数呢,这里直接给出公式:
m
=
−
n
ln
p
ln
2
2
k
=
m
n
ln
2
m=-\frac{n\ln p}{\ln^22}\\ k=\frac mn\ln2
m=−ln22nlnpk=nmln2
k
k
k 为哈希函数个数,
m
m
m 为布隆过滤器长度,
n
n
n 为插入元素的个数,
p
p
p 为误报率。
实现
三个哈希函数的版本:
template<class K, size_t M,
class HashFunc1,
class HashFunc2,
class HashFunc3>
class BloomFilter
{
public:
void Set(const K& key)
{
size_t hash1 = HashFunc1()(key) % M;
size_t hash2 = HashFunc2()(key) % M;
size_t hash3 = HashFunc3()(key) % M;
_bs.set(hash1);
_bs.set(hash2);
_bs.set(hash3);
}
bool Test(const K& key)
{
// 三个位只要有一个为0就是不存在
size_t hash1 = HashFunc1()(key) % M;
if (!_bs.test(hash1)) return false;
size_t hash2 = HashFunc2()(key) % M;
if (!_bs.test(hash2)) return false;
size_t hash3 = HashFunc3()(key) % M;
if (!_bs.test(hash3)) return false;
// 都为1则表示可能存在
return true;
}
private:
bitset<M> _bs;
};
布隆过滤器支持删除吗,如何删除?
布隆过滤器应用场景:
问题:给一个超过100G的log文件,log中存着IP地址,如何找出出现次数最多的IP地址。
这是个 kv 模型,不可使用位图和布隆过滤器。
解决方案:创建100个文件,然后依次读取ip,算出 i = HashFunc(ip) % 100,把这个 ip 放到第 i 个文件里面。这样可以保证同样的 ip 进入同一个的文件,最后使用 unordered_map 对每个文件进行统计(统计完一个文件,clear,再统计下一个文件)。
缺陷:
某个小文件太大
原因:
解决方案:
应用:给两个文件,分别有100亿个ip,我们只有1G内存,如何找两个文件的交集
解决:创建A0 A1 ··· A999,B0 B1 ··· B999,共2000个小文件,分别计算两个文件中的 i = HashFunc(ip) % 1000,分别放到Ai,Bi中,最后对每一对小文件求交集。