哈希位图和布隆过滤器都是常用的概率数据结构,用于高效地判断一个元素是否存在于一个集合当中,但它们在实现方法和各自的优缺点上有所区别。
哈希位图(Hash Bitmap)是由一个位数组构成,每个元素(通常是一个整数)被映射到位数组中的某个位置。对于集合中的每个元素,通过哈希函数将其映射到位数组的对应位置,并将该位置标记为已经存在。判断一个元素是否存在时,只需检查位数组中对应的位置是否被标记,若被标记则表示元素存在,否则表示元素不存在
哈希位图(Hash Bitmap)是由一个位数组构成,每个元素(通常是一个整数)被映射到位数组中的某个位置。对于集合中的每个元素,通过哈希函数将其映射到位数组的对应位置,并将该位置标记为已经存在。判断一个元素是否存在时,只需检查位数组中对应的位置是否被标记,若被标记则表示元素存在,否则表示元素不存在。
当我们存储整数时,因为一个整数为四个bit位,一个比特位八个字节,这样算下来占存的空间极大,我们可以使用
char
类型进行存储,因为char
只占一个字节。
我们可以将数组按字节来进行归类,就如上图来说
当我们要进行存储X的时候,我们先用 X / 8 来算出它在哪个比特位,然后再进行X % 8 来计算出他在这个bit位的第几个字节,计算出后将此位置设置为1,标记此数字
优点
缺点
- 快速查找某个数据是否在一个集合中
- 排序 + 去重
- 求两个集合的交集、并集等
- 操作系统中磁盘块标记
例:
给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
分析:首先不要受到题目的迷惑,100亿整数中包括很多重复数据,实际范围是0到0xffffffff。
512M左右就可以表示数据是否存在了。
代码逻辑分析:
用两个位图来放两文件中的数据,用两个位图中的对应位置表示不同的状态:
00表示两文件中都没有某个数据,10表示在一个位图中出现过,01表示在两个位图中都出现了,这种状态也就是交集数据的状态。
using namespace std;
//普通类型模板
template<size_t N>
class bitset
{
public:
bitset()
{
_bits.resize((N / 8) + 1, 0);
}
//将对应的值按照位图存放
void set(size_t x)
{
size_t i = x / 8;//计算x映射的位在第i个char数组位置
size_t j = x % 8;//计算x映射的位在这个char的第j个比特位
_bits[i] |= (1 << j);
}
void reset(size_t x)
{
size_t i = x / 8;//计算x映射的位在第i个char数组位置
size_t j = x % 8;//计算x映射的位在这个char的第j个比特位
_bits[i] &= ~(1 << j);
}
//检查是否将位图修改
bool test(size_t x)
{
size_t i = x / 8;//计算x映射的位在第i个char数组位置
size_t j = x % 8;//计算x映射的位在这个char的第j个比特位
return _bits[i] & (1 << j);
}
private:
vector<char> _bits;
};
template<size_t N>
class twobitset
{
public:
//查找唯一出现一次的数
void set(size_t x)
{
// 00 -> 01
if (_bs1.test(x) == false
&& _bs2.test(x) == false)
{
_bs2.set(x);
}
else
{
//01 -> 10
_bs1.set(x);
_bs2.reset(x);
}
}
、
void Print()
{
for (size_t i = 0; i < N; ++i)
{
if (_bs2.test(i))
{
cout << i << endl;
}
}
}
void Print_more()
{
for (size_t i = 0; i < N; ++i)
{
if (_bs1.test(i))
{
cout << i << endl;
}
}
}
private:
bitset<N> _bs1;
bitset<N> _bs2;
};
void test_twobitset()
{
int a[] = { 1,1,2,2,3,3,4,4,5,5,6,6,7 };
twobitset<20> t;
for (auto e : a)
{
t.set(e);
}
t.Print();
t.Print_more();
}
布隆过滤器(Bloom Filter)是一种概率型数据结构,基于位数组和多个哈希函数实现。对于集合中的每个元素,通过多个哈希函数将其映射到位数组的多个位置,并将对应位置标记为已经存在。判断一个元素是否存在时,需要对该元素进行多次哈希,并检查所有对应位置是否都被标记,若有任何一个位置未被标记,则表示元素不存在。
在注册账号设置昵称的时候,为了保证每个用户昵称的唯一性,系统必须检测你输入的昵称是否被使用过,这本质就是一个key的模型,我们只需要判断这个昵称被用过,还是没被用过。
位图虽然能够大大节省内存空间,但由于字符串的组合形式太多了,一个字符的取值有256种,而一个数字的取值只有10种,因此无论通过何种哈希算法将字符串转换成整型都不可避免会存在哈希冲突。
这里的哈希冲突就是不同的昵称最终被转换成了相同的整型,此时就可能会引发误判,即某个昵称明明没有被使用过,却被系统判定为已经使用过了,于是就出现了布隆过滤器。
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询。
假设布隆过滤器使用三个哈希函数进行映射,那么“张三”这个昵称被使用后位图中会有三个比特位会被置1,当有人要使用“李四”这个昵称时,就算前两个哈希函数计算出来的位置都产生了冲突,但由于第三个哈希函数计算出的比特位的值为0,此时系统就会判定“李四”这个昵称没有被使用过。
但随着位图中添加的数据不断增多,位图中1的个数也在不断增多,此时就会导致误判的概率增加。
比如“张三”和“李四”都添加到位图中后,当有人要使用“王五”这个昵称时,虽然“王五”计算出来的三个位置既不和“张三”完全一样,也不和“李四”完全一样,但“王五”计算出来的三个位置分别被“张三”和“李四”占用了,此时系统也会误判为“王五”这个昵称已经被使用过了。
布隆过滤器的特点
如何控制误判率
那应该如何选择哈希函数的个数和布隆过滤器的长度呢,有人通过计算后得出了以下关系式:
其中k为哈希函数个数,m为布隆过滤器长度,n为插入的元素个数,p为误判率。
我们这里可以大概估算一下,如果使用3个哈希函数,即k的值为3,l n 2 的值我们取0.7,那么 m 和 n的关系大概是m = 4 × n ,也就是布隆过滤器的长度应该是插入元素个数的4倍。
//布隆过滤器
//N位最多插入的key个数据
template<size_t N, class K = string,
class Hash1 = BKDRHash,
class Hash2 = APHash,
class Hash3 = DJBHash>
class BloomFilter
{
public:
void set(const K& key)
{
size_t len = N * _x;
size_t hash1 = Hash1()(key) % len;
_bs.set(hash1);
size_t hash2 = Hash2()(key) % len;
_bs.set(hash2);
size_t hash3 = Hash3()(key) % len;
_bs.set(hash3);
cout << hash1 << " " << hash2 << " " << hash3 << endl;
}
bool test(const K& key)
{
size_t len = N * _x;
size_t hash1 = Hash1()(key) % len;
if (!_bs.test(hash1))
{
return false;
}
size_t hash2 = Hash2()(key) % len;
if (!_bs.test(hash2))
{
return false;
}
size_t hash3 = Hash3()(key) % len;
if (!_bs.test(hash3))
{
return false;
}
return true; //key对应的三个位都被设置,key存在(可能误判)
}
private:
static const size_t _x = 4;
bitset<N * _x> _bs;//求布隆过滤器长度
};
哈希布隆图相比于位图在空间使用上更加高效,这是因为哈希布隆图利用了多个哈希函数和位数组的重复利用,减少了所需的内存空间。
首先,让我们对比一下位图和哈希布隆图的实现方式:
位图:位图是一个固定大小的位数组,数组中的每个位置表示一个元素,当元素存在时,对应位置的位值为1;当元素不存在时,对应位置的位值为0。位图的大小与集合中元素的总数相一致,每个元素都需要一个位置来进行标记。
哈希布隆图:哈希布隆图也是一个位数组,但与位图不同的是,哈希布隆图的位数组长度通常会远远小于集合中元素的总数。它利用了多个哈希函数,每个元素通过这些哈希函数映射到位数组中的多个位置,将这些位置标记为1。判断一个元素是否存在时,需要对该元素进行多次哈希,并检查所有对应位置是否都被标记。
那么为什么哈希布隆图能够在空间上节省?
多个哈希函数:哈希布隆图使用多个哈希函数,将每个元素映射到位数组的多个位置上。通过这种方式,可以减少冲突的可能性,使得位数组中的每个位置都能够被更多元素共享,从而减少了位图中的重复位。
位数组的重复利用:由于每个元素都会被多次哈希并占用多个位置,在位数组中的某个位置可能被多个元素标记。利用这种重复标记的方式,哈希布隆图可以在相对较小的位数组中表示更多的元素。
举个例子来说明,假设我们要表示一个集合,其中包含100个元素。如果使用位图,我们需要一个大小为100的位数组,每个元素都占用一个位;而使用布隆过滤器,我们可以选择一个相对较小的位数组,比如大小为50,并使用多个哈希函数将元素映射到位数组的多个位置,每个元素可以占用多个位。通过合理的设计,可以在仅占用50个位的情况下,仍然能够高效地判断集合中的元素是否存在。
需要注意的是,哈希布隆图的节省空间是以牺牲一定的查询准确率为代价的。由于哈希冲突和位数组的重复利用,布隆过滤器可能存在一定的误判率,即判断元素不存在时,仍有一定概率判断为存在。因此,在使用哈希布隆图时需要权衡空间利用和查询准确率之间的关系。