位图其实就是哈希表的变形
,当需要映射的数据过大时,如果要把数据都映射并存储,需要很大的空间,但是如果我们并不存储数据**,只关心数据的存在状态,那么我们就可以用比特位来标记数据的存在状态,大大节省内存
位图通常情况下用在数据量庞大,且数据不重复的情景下判断某个数据是否存在
。
底层数据可以用vector存储,方便管理空间
根据模板参数传入要开的位数N,创建一个N位的位图,把位图中所有位初始化为0
一个整形有32个比特位,所有我们需要用到N/32+1
个整形(+1是为了防止N不是32整数被的情况下少开位数
)
template<size_t N>
class BitSet {
public:
BitSet() {
_bits.resize(N / 32 + 1, 0);
}
private:
std::vector<int> _bits;
};
set就是把需要设置的数的状态为置1
由于我们用的是整形存储,所以首先要算出这个状态位在第i个整数
的第j个比特位
然后再**把1左移j位
,然后和第i个整数进行或等运算
**
//把x映射的位标记为1
void Set(size_t x) {
assert(x < N);
//算出x映射的位在第几个整数
size_t i = x / 32;
//算出x映射的位在这个整数的第几个位
size_t j = x % 32;
//_bit[i] 的第j位标记成1,并且不影响它的其他位
_bits[i] |= (1 << j);
}
reset就是把需要设置的状态置为0
和set一样算出这个状态位在第i个整数的第j个比特位
然后把1左移j位,并且取反
,然后进行与等运算
//把x映射的位标记为0
void Reset(size_t x) {
assert(x < N);
size_t i = x / 32;
size_t j = x % 32;
//_bit[i] 的第j位标记成0,并且不影响它的其他位
_bits[i] &= (~(1 << j));
}
test是查看这个数的状态位是否为1
,也就是这个数在位图中是否被设置
算出这个状态位在第i个整数的第j个比特位
然后把1左移j位,进行与运算
bool Test(size_t x) {
assert(x < N);
size_t i = x / 32;
size_t j = x % 32;
//如果第j位是1,结果是非0,就是真
//如果第j位是0,结果是0,就是假
return _bits[i] & (1 << j);
}
template<size_t N>
class BitSet {
public:
BitSet() {
_bits.resize(N / 32 + 1, 0);
}
//把x映射的位标记为1
void Set(size_t x) {
assert(x < N);
//算出x映射的位在第几个整数
size_t i = x / 32;
//算出x映射的位在这个整数的第几个位
size_t j = x % 32;
//_bit[i] 的第j位标记成1,并且不影响它的其他位
_bits[i] |= (1 << j);
}
//把x映射的位标记为0
void Reset(size_t x) {
assert(x < N);
size_t i = x / 32;
size_t j = x % 32;
//_bit[i] 的第j位标记成0,并且不影响它的其他位
_bits[i] &= (~(1 << j));
}
bool Test(size_t x) {
assert(x < N);
size_t i = x / 32;
size_t j = x % 32;
//如果第j位是1,结果是非0,就是真
//如果第j位是0,结果是0,就是假
return _bits[i] & (1 << j);
}
private:
std::vector<int> _bits;
};
位图可以把数据的存在状态进行映射,速度很快,也非常节省空间,但是位图只能映射整形数据
,如果要映射的数据是字符串或者自定义类型,那么使用位图就无法进行映射,为了解决位图不能映射字符串的问题
,就有了布隆过滤器
布隆过滤器是布隆(Burton Howard Bloom)
在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在
”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
要想把字符串映射到布隆过滤器,就得把字符串转换成整数
,使用字符串哈希算法,可以完成这个操作,但是字符串哈希算法转换成整形,会产生哈希冲突
,也就是不同的字符串算出的映射地址是相同的,这时数据的存在状态就会存在误判
,这个问题是无法避免
的。
布隆提出了一个解决办法,可以降低误判的概率
,那就是把一个字符串用多个哈希函数进行运算,计算出多个哈希地址,在进行检测时,多个位置判断都是存在,那么就判断数据可能存在
,降低了哈希冲突带来的误判概率,但是这种方式并不能确定这个数据一定存在
,但是只要判断出三个位置一个状态为0
,那么这个数据就一定不存在
也就是说
存在
的情况不一定准确
,可能会存在误判
不存在
的情况一定准确
,不会误判
优点
O(K)
, (K为哈希函数的个数,一般比较小),与数据量大小无关误判
时,布隆过滤器比其他数据结构有这很大的空间优势缺陷
误判率
,即存在假阳性(False Position),即不能准确判断元素是否在集合中布隆过滤器需要用到多种字符串哈希算法,这里用三种哈希算法
struct HashBKDR{
//BKDRHash算法
size_t operator()(const std::string& s) {
size_t value = 0;
for (auto ch : s) {
value += ch;
value *= 131;
}
return value;
}
};
struct HashAP {
//AP Hash算法
size_t operator()(const std::string& s) {
register size_t hash = 0;
size_t ch;
for (long i = 0; i < s.size(); i++){
ch = s[i];
if ((i & 1) == 0){
hash ^= ((hash << 7) ^ ch ^ (hash >> 3));
}
else{
hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));
}
}
return hash;
}
};
struct HashDJB {
//DJB Hash算法
size_t operator()(const std::string& s) {
register size_t hash = 5381;
for(auto ch : s)
{
hash += (hash << 5) + ch;
}
return hash;
}
};
更多字符串哈希算法参考
字符串哈希算法
布隆过滤器是位图的拓展
,底层用位图实现
template<size_t N, class K = std::string,
class Hash1 = HashBKDR,
class Hash2 = HashAP,
class Hash3 = HashDJB>
class BloomFither {
//......
private:
BitSet<N> _bitset;
};
set就是把三个哈希算法算出的哈希地址全都映射进布隆过滤器
void Set(const K& key) {
size_t i1 = Hash1()(key) % N;
size_t i2 = Hash2()(key) % N;
size_t i3 = Hash3()(key) % N;
_bitset.Set(i1);
_bitset.Set(i2);
_bitset.Set(i3);
}
test就是判断数据是否在布隆过滤器,数据的存在性会有误判,但是不存在性不会误判
bool Test(const K& key) {
size_t i1 = Hash1()(key) % N;
if (_bitset.Test(i1) == false) {
return false;
}
size_t i2 = Hash2()(key) % N;
if (_bitset.Test(i2) == false) {
return false;
}
size_t i3 = Hash3()(key) % N;
if (_bitset.Test(i3) == false) {
return false;
}
//这里三个位都存在,有可能是其他key占了,存在是不准确的,存在误判
return true;
}
由于布隆过滤器三个位置可能会使用到别的元素的哈希地址,删除会影响别的元素的映射
,所以布隆过滤器一般不提供删除操作
但是有一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作
#pragma once
#include"BitSet.h"
using namespace std;
struct HashBKDR{
//BKDRHash算法
size_t operator()(const std::string& s) {
size_t value = 0;
for (auto ch : s) {
value += ch;
value *= 131;
}
return value;
}
};
struct HashAP {
//AP Hash算法
size_t operator()(const std::string& s) {
register size_t hash = 0;
size_t ch;
for (long i = 0; i < s.size(); i++){
ch = s[i];
if ((i & 1) == 0){
hash ^= ((hash << 7) ^ ch ^ (hash >> 3));
}
else{
hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));
}
}
return hash;
}
};
struct HashDJB {
//DJB Hash算法
size_t operator()(const std::string& s) {
register size_t hash = 5381;
for(auto ch : s)
{
hash += (hash << 5) + ch;
}
return hash;
}
};
template<size_t N, class K = std::string,
class Hash1 = HashBKDR,
class Hash2 = HashAP,
class Hash3 = HashDJB>
class BloomFither {
public:
void Set(const K& key) {
size_t i1 = Hash1()(key) % N;
size_t i2 = Hash2()(key) % N;
size_t i3 = Hash3()(key) % N;
_bitset.Set(i1);
_bitset.Set(i2);
_bitset.Set(i3);
}
bool Test(const K& key) {
size_t i1 = Hash1()(key) % N;
if (_bitset.Test(i1) == false) {
return false;
}
size_t i2 = Hash2()(key) % N;
if (_bitset.Test(i2) == false) {
return false;
}
size_t i3 = Hash3()(key) % N;
if (_bitset.Test(i3) == false) {
return false;
}
//这里三个位都存在,有可能是其他key占了,存在是不准确的,存在误判
return true;
}
private:
BitSet<N> _bitset;
};