目录
位图
实现思路
模拟实现
类模板
构造函数
set
reset
test
twobitset 实现
成员变量
set
is_once
布隆过滤器
成员变量与模板
set
test
删除
位图
位图是哈希的另一种表现,与我们常用的 set map 的思想还是基本一样的,下面我们看一下简单位图的实现:
实现思路
-
在位图的实现中,我们想要用一个比特位,来表示一个数据的状态信息(也就是是否存在)。
-
所以一个位图并不能表示一个数据存储/出现过的次数。
-
但是在 C/C++ 的内置类型中并未有单位是比特位的类型,所以这个还是需要我们开空间的时候自己计算。
-
其中开空间我们可以使用我们已知的数据类型去开空间例如:char、int。
-
虽然上面只说了两种数据类型,但是并不是只有这两种可以使用,只是这两种比较常见,且容易控制。
-
如果是 int 的话,那么开一个 int 空间就是开了 32 个比特位,也就是一个 int 可以存储 32 个数据的状态。
模拟实现
-
当我们使用位图的时候,由于只有用户知道自己想要存储的数据的范围,所以位图的空间需要有用户来决定
-
所以位图需要一个非类型模板参数用来表示可以存储的数据范围
类模板
template<size_t N>class bitset{public:private:vector<int> _a;};
-
这里我们使用 int 类型来控制。
-
这里的非类型模板参数传过来的值,就可以在构造函数里面为 _a 开一段所需要的空间。
构造函数
bitset(){_a.resize(N / 32 + 1);}
-
由于这里使用的是 int 所以我们开辟的空间需要 N / 32,但是 N / 32 是向下取整,所以如果这样的话,我们还需要对除后的值多开一个 int 类型,防止开的空间不够。
set
-
set 就是将一个值设置到位图中
-
设置进位图就是将对应的比特位设置为 1
-
我们只需要让该比特位或 1 ,其他位为 0 即可
-
我们可以将 1 像左移动到该比特位,然后与该值或等,由于或是有1为1,所以我们可以将该比特位设置为1
-
在设置之前,我们还需要计算出对应的比特位的位置吗,由于我们使用的是 int 所以我们想知道到第几个比特位,我们就可以对该值进行除32 就是在对应的数组位置,然后我们知道了对应的数组位置,我们还需要知道该比特位在该数组位置的第几个比特位,所以我们还可以用该值对 32 进行取模操作,我们就知道对应的比特位了
-
下面看代码会很容易理解
void set(int n){int i = n / 32;int j = n % 32;
_a[i] |= 1 << j;}
reset
-
reset 就是将一个比特位移除,也就是将该比特位设置为 0
-
想要将一个比特位设置为 0 ,我们可以对该比特位与等一个0,其他位为1即可
-
还是看代码容易理解,或者可以自己写一下
void reset(int n){int i = n / 32;int j = n % 32;
_a[i] &= ~(1 << j);}
test
-
test 就是用来探测一个值是否在位图中,如果为1 则在位图中,如果为 0 ,则不在位图中
-
其实我们想知道一个比特位在位图中是否为1或者是0,我们可以对该比特位与1,如果结果是1,则说明该比特位表示的值是存在的,如果为0,表示该比特位表示的值并不存在
bool test(int n){int i = n / 32;int j = n % 32;
return _a[i] & 1 << j;}
如果我们这里有一堆数字,我们想要判断这些数字中只出现了一次的值是什么,那么我们怎么使用位图来解决?
思路:由于一个位图只能表示在不在,那么我们可不可以有两个比特位,入如果有两个比特位,比那么我们就可以表示很多情况了 例如: 0 0 0 1 1 0 1 1,有上面四种情况,但是我们只需要判断出现了一次的数字,那么我们其实用不了四种情况,我们只需要三个就可以了, 0 0 表示没有出现过, 0 1 表示出现了一次, 1 0 表示出现了两次或者是两次以上,所以我们可以使用两个位图来表示:
twobitset 实现
成员变量
-
上面说了,我们需要用两个位图来表示,所以我们需要两个位图
template<size_t N>class twobitset{public:private:bitset<N> _b1;bitset<N> _b2;};
set
-
这里的 set 就需要分情况了,如果一次都没有出现,那么就是让 _b1 变为 0, _b2 变为 1,也就是 0 1
-
如果这里已经出现了一次了,那么就是让 0 1 变为 1 0 所以我们需要让_b1 变为 1, _b2 变为 0 也即是 1 0
void set(int n){// 0 0 -> 0 1if (!_b1.test(n) && !_b2.test(n)){_b2.set(n);} // 0 1 -> 1 0else if (!_b1.test(n) && _b2.test(n)){_b1.set(n);_b2.reset(n);}}
is_once
-
既然我们需要知道师傅出现了一次,那么我们当然需要有一个函数用来判断是否出现了一次
-
如果 _b1 是 0 _b2 是 1 那么就表明出现了一次,否则就不是一次
bool is_once(int n){return !_b1.test(n) && _b2.test(n);}
布隆过滤器
-
位图只能用来表示整数是否存在,那么如果还想表示其他类型,例如:字符串?
-
布隆过滤器就可以用来解决这个问题
-
这里可以使用 hash 函数对字符串进行映射,然后将映射出来的值作为整数放到位图里面
-
但是,我们知道hash函数是会出现冲突的,如果字符串很多的话,那么冲突是很多的
-
其中,我们可以将一个字符串使用多个hahs函数映射出来不同的值,然后将hahs函数映射出来的值都放到位图里面,这样有几个hahs函数就可以映射几个比特位,这样就可以减少冲突
-
怎么探测是否存在呢?既然一个字符串映射了好几个hash比特位,所以我们探测是否存在的时候,也需要探测这些对应的比特位是否存在,只要有一个比特位不存在,则说明该字符串不存在
-
但是即使是通过了多次的映射,也还会产生冲突,剩下的介绍冲突的做法就是增加空间了
成员变量与模板
-
我们可以将hash函数作为模板传过去,可以使用仿函数来调用这些函数
-
我们的成员变量就是一个位图
template<size_t N, class K = string, class Hash1 = BKDRHash, class Hash2 = APHash, class Hash3 = DJBHash>class bloomfilter{public:private:bitset<N> _bs;};
我们可以看一下这些hash函数
struct BKDRHash
{size_t operator()(const string& str){size_t hash = 0;for (char ch : str){hash = hash * 131 + ch;}return hash;}
};
struct APHash
{size_t operator()(const string& str){size_t hash = 0;size_t ch;for (long i = 0; i < str.size(); i++){if ((i & 1) == 0){hash ^= ((hash << 7) ^ str[i] ^ (hash >> 3));}else{hash ^= (~((hash << 11) ^ str[i] ^ (hash >> 5)));}}return hash;}
};
struct DJBHash
{size_t operator()(const string& str){if (str.size() == 0)return 0;size_t hash = 5381;for (char ch : str){hash += (hash << 5) + ch;}return hash;}
};
set
-
set 函数就是有几个 hash 函数,我们就映射几个位置,但是也不是hash函数越多越好,只是在特定的范围内,hash函数多了可以减少冲突
-
上面我们使用了3个hash函数
-
上面的hash函数可以参考该网站:
[各种字符串 hash 函数] https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html
void set(const K& key){size_t hash1 = Hash1()(key) % N;_bs.set(hash1);size_t hash2 = Hash2()(key) % N;_bs.set(hash2);size_t hash3 = Hash3()(key) % N;_bs.set(hash3);cout << hash1 << " " << hash2 << " " << hash3 << endl;}
test
-
在布隆过滤器中,一个字符串映射了多个位置,所以如果想要探测该字符串是否存在,那么就需要探测这些是否位置是否被映射,只要有一个为假,那么该字符串便不存在
-
我们前面说过,布隆过滤器是有冲突的,所以又可能有一个不存在的字符串被识别为存在,所以说,判断是否存在不是很准确的
-
但是不存在是存在是准确的
bool test(const K& key){size_t hash1 = Hash1()(key) % N;size_t hash2 = Hash2()(key) % N;size_t hash3 = Hash3()(key) % N;return _bs.test(hash1) && _bs.test(hash2) && _bs.test(hash3);}
删除
-
如果我们想要删除布隆过滤器中的一个值,那么我们怎么删除?
-
其实布隆过滤器中的值是不能删除的,为什么?我们前面为了减少冲突,我们是一个值映射了多个位置。
-
所以我们的一个比特位可能被多个值映射过,如果我们直接删除了的话,我们就可能导致其他值也找不到,所以不能删除。
-
那么如果想要实现删除呢?
-
我们可以多搞一些比特位,其中两个比特位可以表示到 3,所以如果我们想要删除,我们至少要 8 个比特位,而且 8 个比特位也不一定就足够了,如果我们搞这么多比特位,那么范围违背了我们的原则,我们使用布隆过滤器就是为了占用少量的空间,此时反而使用了很多空间,那么显然是不划算的。