目录
前言
位图的概念
经典面试题目
位图的模拟实现
set()
reset()
test()
位图整体代码
位图的应用
位图的优缺点
布隆过滤器
布隆过滤器的概念
哈希函数的个数与布隆过滤器长度的关系
布隆过滤器的模拟实现
插入
查找
删除
布隆过滤器整体代码
前言
哈希本质是一种映射(绝对映射/相对映射)的思想,哈希思想的应用之一便是位图,位图需要对比特位进行操作,因此需要回顾整型数据在内存中的存储与移位运算;
思考:如何将整型数据的第k个比特位设置为1或者0并且不影响其余的比特位 ?
无论当前机器是大端存储或者小端存储,由于数字1的最低的比特位为1,左移始终向高位移动,因此数字1先首先左移k个比特位,其次和原序列做或运算便设置第k个比特位为1;
同理,首先将数字1左移k个比特位,然后按位取反此时数字1所对应的二进制序列只有第k个比特位为0,其余的比特位皆为1,最后和原序列做与运算便可设置第k个比特位为0;
位图的概念
位图是一种基于位操作的数据结构,用于表示一组元素的集合信息;它通常是一个仅包含0和1的数组,其中每个元素对应集合中的一个元素;位图中的每个位代表一个元素是否存在于集合中当元素存在时,对应位的值为1;不存在时,对应位的值为0;
经典面试题目
40亿个无符号整数,每个大小4个字节,则一共占用160亿字节,而1GB大约为10亿字节,则总共需要大约16GB,多数电脑的内存根本无法存放这些数据,若将数据存放于磁盘,进行外排序与二分查找,若在磁盘上进行,磁盘加载数据的速度缓慢,会导致效率降低,因此便采用位图解决;
内存中表示一个值是否存在的最小单位为比特位,由于数据范围为0 ~ 2^(32)-1,因此开辟2^(32)个比特位的空间,数据的值与存储位置构成绝对映射来标识该值是否存在;
位图的模拟实现
位图的底层结构为数组,采用vector充当底层容器,数组空间的开辟最小以字节为单位,因此既可以开辟整型数组也可以开辟字符型数组;本文采用开辟整型数组;
//非类型模版参数N指定开辟多少比特位的空间
template<size_t N>
class BitSet
{
public:
//构造函数中需要开辟空间,否则vector大小为0
BitSet()
{
_bits.resize(N / 32 + 1, 0);
}
private:
vector<int> _bits;//开辟整型数组空间
};
- 非类型模版参数N指定比特位的个数,而构造函数开辟的整型变量的个数,所以需要N/32;
- 由于N/32的结果不是整数时会取整而抛弃小数部分,所以需要N/32+1,增加1个整型确保比特位足够映射集合中的数值;
set()
set设置x为存在,即将x映射的比特位设置为1;
void set(size_t x)
{
//计算x在第i个整型
size_t i = x / 32;
//计算x在第j个比特位
size_t j = x % 32;
_bits[i] = _bits[i] | (1 << j);
}
reset()
reset设置x为不存在,即将x映射的比特位设置为0;
清空位图中指定位的方法如下:
- 计算出该位位于第 i 个整型的第 j 个比特位;
- 将1左移 j 位然后按位取反最后和第 i 个整数进行与运算;
//将x映射的比特位设置为0
void reset(size_t x)
{
//计算x在第i个整型
size_t i = x / 32;
//计算x在第j个比特位
size_t j = x % 32;
_bits[i] = _bits[i] & ~(1 << j);
}
test()
检测某个比特位所标识的数值是否存在,存在则为真,否则为假;
获取位图中指定位的状态的方法如下:
- 计算出该位位于第 i 个整数的第 j 个比特位;
- 将1左移 j 位后与第 i 个整数进行与运算;
- 若结果非0,则该位所标识的数值存在,否则此比特位所标识的数值不存在;
位图整体代码
template<size_t N>
class BitSet
{
public:
BitSet()
{
_bits.resize(N / 32 + 1, 0);
}
//将x映射的比特位设置为1
void set(size_t x)
{
//计算x在第i个整型
size_t i = x / 32;
//计算x在第j个比特位
size_t j = x % 32;
_bits[i] = _bits[i] | (1 << j);
}
//将x映射的比特位设置为0
void reset(size_t x)
{
size_t i = x / 32;
size_t j = x % 32;
_bits[i] = _bits[i] & ~(1 << j);
}
//检测数值x是否存在
bool test(size_t x)
{
size_t i = x / 32;
size_t j = x % 32;
return _bits[i] & (1 << j);
}
private:
vector<int> _bits;
};
位图的应用
位图的一个比特位只有两种状态标识数据是否存在,而统计次数数据可能出现0次、出现1次,出现1次以上具有三种状态,因此需要两个比特位来标识这三种状态;
template<size_t N>
class two_bit_set
{
public:
void set(size_t x)
{
// 原先为00,数据x出现后变为01
if (_bs1.test(x) == false
&& _bs2.test(x) == false)
{
_bs2.set(x);
}
else if (_bs1.test(x) == false
&& _bs2.test(x) == true)
{
//原先为01,数据x出现后变为10
_bs1.set(x);
_bs2.reset(x);
}
//一次及以上不做处理
}
//数值x出现0次返回0,出现1次返回1,出现1次及以上返回2
int test(size_t x)
{
if (_bs1.test(x) == false
&& _bs2.test(x) == false)
{
return 0;
}
else if (_bs1.test(x) == false
&& _bs2.test(x) == true)
{
return 1;
}
else
{
return 2; // 2次及以上
}
}
private:
BitSet<N> _bs1;
BitSet<N> _bs2;
};
位图的优缺点
优点:节省空间,效率高
缺点:一般要求数据范围相对集中,否则会导致空间消耗很大,位图只能处理整型数据,若内容编号为字符串,无法处理;
布隆过滤器
对于位图而言,只能处理整型数据,因为数据的数值采用 【直接定址法】计算哈希值几乎不会产生哈希冲突的问题,虽然字符串可以通过不同的哈希函数将字符串转换为整型,但是字符串的组合形式复杂多样,无论通过哪种哈希函数都不可避免地会出现大量哈希冲突;
此处的哈希冲突指不同的字符串被转换为相同的整型,此时便可能产生误判,即某个字符串明明不在数据集合中,却被系统判定为存在,于是诞生了布隆过滤器;
- 位图中存在:不一定真正存在
如上图中"百度"和"百渡"转换为整型数值相同,那么映射的位置也相同,所以位图中第1234个比特位是1,就可以说"百度"和"百渡"都存在,但实际上是"百度"存在,而"百渡"不存在,于是产生了误判;
- 位图不存在:必然不存在
若字符串"字节"转换为整型后与之对应的位图上的比特位为0,则说明"字节"不存在;
布隆过滤器的概念
布隆过滤器:当一个数据映射到位图中时,采用多个哈希函数将其映射到多个比特位,当判断一个数据是否在位图当中时,需要分别根据这些哈希函数计算出对应的比特位,如果根据不同的哈希函数计算的比特位都设置为1则判定为该数据存在,否则判定为该数据不存在;
布隆过滤器使用多个哈希函数进行映射,目的在于【降低哈希冲突的概率】,一个哈希函数产生冲突的概率相对较高,但多个哈希函数同时产生冲突的概率会下降;
布隆过滤器极大的降低了哈希冲突的概率,但是仍然可能会产生误判:
- 当布隆过滤器判断一个数据存在可能是不准确的,因为这个数据对应的比特位可能被其他一个数据或多个数据占用;
- 当布隆过滤器判断一个数据不存在是准确的,因为该数据存在那么该数据对应的比特位都应该已经被设置为1;
哈希函数的个数与布隆过滤器长度的关系
问题是到底该创建多少个比特位的位图(布隆过滤器长度),又应该使用多少个哈希函数来映射一个字符串呢?
选取k=3,即设计3个哈希函数,则m约等于4.5倍n
布隆过滤器的模拟实现
template<size_t N,
class K=string, //数据默认为字符串
class Hash1 = BKDRHash,//三种字符串哈希算法(将字符串转换为整型)
class Hash2 = APHash,
class Hash3 = DJBHash>
class bloomfilter
{
public:
private:
static const size_t M = 5 * N;//M布隆过滤器长度=5*插入元素的个数
//STL库中位图实现为静态数组(即int arr[]),存储在对象中,数据量大时可能会导致栈溢出,所以使用new开辟堆空间避免栈溢出
std::bitset<M>* _bs = new std::bitset<M>;
};
选择三种字符串哈希算法,原文链接:https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html
//BKDR版本
struct BKDRHash
{
size_t operator()(const string& s)
{
size_t value = 0;
for (auto ch : s)
{
value = value * 131 + ch;
}
return value;
}
};
//AP版本
struct APHash
{
size_t operator()(const string& s)
{
size_t value = 0;
for (size_t i = 0; i < s.size(); i++)
{
if ((i & 1) == 0)
{
value ^= ((value << 7) ^ s[i] ^ (value >> 3));
}
else
{
value ^= (~((value << 11) ^ s[i] ^ (value >> 5)));
}
}
return value;
}
};
//DJB版本
struct DJBHash
{
size_t operator()(const string& s)
{
if (s.empty())
return 0;
size_t value = 5381;
for (auto ch : s)
{
value += (value << 5) + ch;
}
return value;
}
};
插入
当元素插入到布隆过滤器时,布隆过滤器需要提供一个set接口,插入元素时,需要通过三个哈希函数分别计算出该元素对应的三个比特位,然后将位图中的映射的三个比特位设置为1即可;
void set(const K& key)
{
size_t hash1 = Hash1()(key) % M;
size_t hash2 = Hash2()(key) % M;
size_t hash3 = Hash3()(key) % M;
_bs->set(hash1);
_bs->set(hash2);
_bs->set(hash3);
}
查找
布隆过滤器需要提供一个test接口,用于检测某个元素是否在布隆过滤器当中;
检测时,需要通过三个哈希函数分别计算出该元素对应的三个比特位,然后判断位图中映射的三个比特位是否被设置为1;
只要映射的三个比特位当中有一个比特位没有被设置为1则说明该元素一定不存在;
若映射的三个比特位全部被设置为1,则返回true表示该元素存在;
注意:判断存在的情况可能存在误判;
bool test(const K& key)
{
//依次判断key对应的三个位是否被设置
size_t hash1 = Hash1()(key) % M;
if (_bs->test(hash1) == false)
return false;//key一定不存在
size_t hash2 = Hash2()(key) % M;
if (_bs->test(hash2) == false)
return false;//key一定不存在
size_t hash3 = Hash3()(key) % M;
if (_bs->test(hash3) == false)
return false;//key一定不存在
return true; // 存在误判(有可能3个位都是跟别人冲突的,所以误判)
}
删除
布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素;
"百度"和"字节"映射的比特位都有第4个比特位,删除上图中"字节"元素,如果直接将该元素所对应的二进制比特位置0,则"百度"元素也被删除,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠;
一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作;
总结:布隆过滤器最好不要支持删除操作
布隆过滤器整体代码
struct BKDRHash
{
size_t operator()(const string& s)
{
size_t value = 0;
for (auto ch : s)
{
value = value * 131 + ch;
}
return value;
}
};
struct APHash
{
size_t operator()(const string& s)
{
size_t value = 0;
for (size_t i = 0; i < s.size(); i++)
{
if ((i & 1) == 0)
{
value ^= ((value << 7) ^ s[i] ^ (value >> 3));
}
else
{
value ^= (~((value << 11) ^ s[i] ^ (value >> 5)));
}
}
return value;
}
};
struct DJBHash
{
size_t operator()(const string& s)
{
if (s.empty())
return 0;
size_t value = 5381;
for (auto ch : s)
{
value += (value << 5) + ch;
}
return value;
}
};
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 hash1 = Hash1()(key) % M;
size_t hash2 = Hash2()(key) % M;
size_t hash3 = Hash3()(key) % M;
_bs->set(hash1);
_bs->set(hash2);
_bs->set(hash3);
}
bool test(const K& key)
{
size_t hash1 = Hash1()(key) % M;
if (_bs->test(hash1) == false)
return false;
size_t hash2 = Hash2()(key) % M;
if (_bs->test(hash2) == false)
return false;
size_t hash3 = Hash3()(key) % M;
if (_bs->test(hash3) == false)
return false;
return true;
}
private:
static const size_t M = 5 * N;
std::bitset<M>* _bs = new std::bitset<M>;
};
欢迎大家批评指正,博主会持续输出优质内容,谢谢各位观众老爷观看,码字画图不易,希望大家给个一键三连支持~ 你的支持是我创作的不竭动力~