位图及布隆过滤器的模拟实现
位图
位图其实就是一个简化版的哈希表,在很多时候,我们并不关注一个数据出现了多少次,而只是关注这个数据存在与否。所以哈希表太浪费空间了,我们可以用一个比特位来表示一个数据存在与否。
那么在 32 位平台下,一个整数是 4 个字节,共 32 个比特位,那么它就可以表示 32 个数据的状态。
比如我们要处理 100 个数据并且这 100 个数据都是小于等于 100 的,那么就可以申请一个大小为 4 的整型数组就行。从 100 个整型 到 4 个整型我们节约了空间;在大数据的环境下,这种节约更加可以体现。
位图的数组需要多少个整数元素,是由数据里最大的数决定的。
#pragma once
#include <iostream>
#include <vector>
class BitSet
{
public:
BitSet(int range)
:bit_((range >> 5) + 1)
{}
~BitSet() {}
// 置1
void set(int num)
{
if (num > bit_.size())
{
return;
}
int index = num >> 5; // 找到 num 在数组中的位置
int pos = num % 32; // 定位比特位
bit_[index] |= (1 << pos);
}
// 置0
void reset(int num)
{
int index = num >> 5;
int pos = num % 32;
bit_[index] &= ~(1 << pos);
}
bool test(int num)
{
int index = num >> 5;
int pos = num % 32;
return bit_[index] & (1 << pos);
}
private:
std::vector<int> bit_;
};
这是一个简单的位图模拟实现,只实现了三个接口,但是这三个接口也是最主要的。
位图的应用:
- 快速查找某个数据是否在一个集合中
- 排序
- 求两个集合的交集、并集等
布隆过滤器
概念
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你某样东西一定不存在或者可能存在,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
怎么理解呢?
这么说,对于数字来说,我们如果只关注一个数字存在与否,为了节约空间,我们可以选择位图,那么对于字符串来说怎么办?
假如我们只用某一个哈希函数将字符串转换成整数,然后在位图上对应的比特位给 1 就行。那么势必存在另一个不一样的字符串用相同的哈希函数转换成整数跟上一个字符串的整数相同。那么就不能达到我们我们的初衷了。
所以,布隆过滤器的思想就是使用多个哈希函数将一个字符串转换为整数,每个哈希函数算出来一个整数就在位图的响应比特位上给 1 。之后找的时候,还是用之前的多个哈希函数分别算出值来在位图上找,如果存在有一个哈希函数算出来的值对应的比特位为 0, 那么这个字符串一定不存在。如果算出来的所有值对应的比特位都为 1,那么也不一定这个字符串存在。
使用多个哈希函数只是为了降低字符串转换出来的整数相同的概率,并不代表不可能存在两个字符串算出来的值一定相同。
还有一种情况,已经有多个字符串将位图的多个比特位置 1 了, 那么再来一个新的字符串,之前并没有出现过,这个新的字符串使用所有哈希函数算出来的值对应相应的比特位都为 1,这种情况也有可能。
所以:如果存在有一个哈希函数算出来的值对应的比特位为 0, 那么这个字符串一定不存在。如果算出来的所有值对应的比特位都为 1,那么也不一定这个字符串存在。
#include "BitSet.hpp"
template <class K, class HashFun1, class HashFun2, class HashFun3>
class BloomFilter
{
public:
BloomFilter(size_t range)
:bs_(range),
bit_count_(range)
{}
void Set(const K& key)
{
// 先通过哈希函数将 key 转成整数
size_t index1 = HashFun1()(key) % bit_count_;
size_t index2 = HashFun2()(key) % bit_count_;
size_t index3 = HashFun3()(key) % bit_count_;
bs_.set(index1);
bs_.set(index2);
bs_.set(index3);
}
bool Test(const K& key)
{
size_t index1 = HashFun1()(key) % bit_count_;
if (!bs_.test(index1))
return false;
size_t index2 = HashFun2()(key) % bit_count_;
if (!bs_.test(index2))
return false;
size_t index3 = HashFun3()(key) % bit_count_;
if (!bs_.test(index3))
return false;
return true;
}
private:
BitSet bs_;
size_t bit_count_;
};
struct HF1
{
size_t operator()(const std::string& str)
{
size_t hash = 0;
for (const auto& e : str)
{
hash = hash * 65599 + e;
}
return hash;
}
};
struct HF2
{
size_t operator()(const std::string& str)
{
size_t hash = 0;
for (const auto& e : str)
{
hash = hash * 131 + e;
}
return hash;
}
};
struct HF3
{
size_t operator()(const std::string& str)
{
size_t hash = 0;
size_t magic = 63689;
for (const auto& e : str)
{
hash = hash * magic + e;
magic *= 378551;
}
return hash;
}
};
这里我只使用了三个哈希函数。但是哈希函数并不是越多越好:
- 多了会造成来一个新的字符串,但是误判它已经存在。
- 少了可能会造成两个字符串算出来的值对应的比特位相同。
关于布隆过滤器的删除:
布隆过滤器我没有实现删除操作,是因为,如果删除某一个字符串就有可能在删除当前字符串所对应的比特位的同时,也删除了其他字符串所对应的比特位。想想,如果两个字符串求得的哈希值同时都有 0 号比特位,那么如果发生一次删除操作,将 0 号比特位的值置 0 , 那么下次再查找另一个字符串的时候,就会显示不存在。
如果非要实现删除操作,只能改变布隆过滤器的底层,使用数组元素来代替比特位来完成一个引用计数,那么这样就会违反我们节约空间的初衷了。所以我个人还是觉得布隆过滤器不要有删除操作。
布隆过滤器的优点:
-
增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
-
哈希函数相互之间没有关系,方便硬件并行运算
-
布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
-
在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
-
数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
-
使用同一组散列函数的布隆过滤器可以进行交、并、差运算
布隆过滤器的缺陷:
-
有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)
-
不能获取元素本身
-
一般情况下不能从布隆过滤器中删除元素
-
如果采用计数方式删除,可能会存在计数回绕问题
叮~?