布隆过滤器
介绍
在许多场景下,如设置昵称时,往往要求唯一性。这时就需要高效判断该昵称是否被使用过。
-
使用红黑树的kv模型或者哈希表来组织昵称集合,可以,但缺点是可能使用浪费大量内存空间
-
使用位图,将字符串先转换为整型数字,然后映射到具体比特位上,通过0/1状态判断。可以,但缺点是发生哈希冲突,导致不同昵称最终映射到相同的比特位,发生误判。
布隆过滤器:
可以用来告诉你 “某样东西一定不存在或者可能存在”。它是用多个哈希函数,将一个数据映射到位图结构中。提升查询效率,节省大量的内存空间
- 将一个数据通过多个哈希函数映射到多个比特位。
- 查询某个数据是否存在时,通过这些哈希函数映射到对应比特位。如果都为1,则存在;如果有一位为0,则不存在。
- 使用多个不同的哈希函数,可以降低误判率
(如:某个未被使用的昵称,经过多个哈希函数映射后,还是有可能和昵称集合中某些映射到相同比特位,发现全为1,误判:该昵称已经被使用)
随着1的比特位逐渐增多,误判率也会增加,存在(可以忍受的)错误率,通过调整哈希函数和位图大小来平衡。
如何选择合适的k和m值呢?
k为哈希函数个数,m为布隆过滤器长度,n为插入的元素个数,p为误判率
这里有一个公式:
m = − n l n p ( l n 2 ) 2 k = m n l n 2 m=-\frac{nlnp}{(ln2)^2}\\ k=\frac{m}{n}ln2 m=−(ln2)2nlnpk=nmln2
当哈希函数个数 k = 3 k=3 k=3, l n 2 ≈ 0.7 ln2\approx0.7 ln2≈0.7,则 m = 4.3 n m=4.3n m=4.3n,即位图长度应该是插入元素个数的4~5倍
实现
哈希函数
下面三个都是比较经典的字符串转整型的哈希函数
struct HashBKDR
{
// BKDR
size_t operator()(const string& key)
{
size_t val = 0;
for (auto ch : key)
{
val *= 131;
val += ch;
}
return val;
}
};
struct HashAP
{
// BKDR
size_t operator()(const string& key)
{
size_t hash = 0;
for (size_t i = 0; i < key.size(); i++)
{
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ key[i] ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ key[i] ^ (hash >> 5)));
}
}
return hash;
}
};
struct HashDJB
{
// BKDR
size_t operator()(const string& key)
{
size_t hash = 5381;
for (auto ch : key)
{
hash += (hash << 5) + ch;
}
return hash;
}
};
布隆过滤器
// N表示准备要映射N个值
template<size_t N,
class K = string, class Hash1 = HashBKDR, class Hash2 = HashAP, class Hash3 = HashDJB>
class BloomFilter
{
public:
void Set(const K& key)
{
size_t hash1 = Hash1()(key) % (_ratio * N);
_bits->set(hash1);
size_t hash2 = Hash2()(key) % (_ratio * N);
_bits->set(hash2);
size_t hash3 = Hash3()(key) % (_ratio * N);
_bits->set(hash3);
}
bool Test(const K& key)
{
size_t hash1 = Hash1()(key) % (_ratio * N);
if (!_bits->test(hash1))
return false; // 准确的
size_t hash2 = Hash2()(key) % (_ratio * N);
if (!_bits->test(hash2))
return false; // 准确的
size_t hash3 = Hash3()(key) % (_ratio * N);
if (!_bits->test(hash3))
return false; // 准确的
return true; // 可能存在误判
}
// 不支持删除
//void Reset(const K& key);
private:
//开辟_ration * 5个比特位空间,减少误判率
const static size_t _ratio = 5;
std::bitset<_ratio * N>* _bits = new std::bitset<_ratio * N>;
};
删除
布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。(如:删除某个昵称后,将其映射的几个比特位为0,可能会导致其他已使用的昵称被判定为未使用)
小结
优点:不存储元素本身,节约空间,保密,增加和查询效率高
缺点:存在误判
对于已保存在布隆过滤器中的元素,test()不会出现误判;对于未保存的,可能会误判,认为已存在。
使用——题目
除了可以应用在设置昵称。也可以在设置黑名单,阻止非法访问。
1.给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法
近似算法:允许一些误判。将一个文件的所有query(字符串)映射进一个布隆过滤器中,然后使用另一个文件对布隆过滤器进行test(),如果在,就是交集部分。
精确算法:不允许误判。哈希切分,假设每个query占30byte,100亿query需要300亿byte,约300G。内存不能加载那么大的空间,则需要先切分成小块。
2.如何扩展BloomFilter使得它支持删除元素的操作
用多个位表示一个位置,做计数。但是为了支持删除,空间消耗更多,优势削弱了
🦀🦀观看~~