前言
位图和布隆过滤器是基于哈希思想实现的数据结构,他们在很多的方面都有应用,比如:操作系统中的磁盘标记,快速查找某个数据是否在集合中。布隆过滤器可以高效的进行插入和查询,可以告诉你“某样东西一定不存在或者可能存在”。让我们一起来认识一下它们吧。
1.位图
1.1位图的概念
所谓位图就是用每一位来存放某种状态,适用于海量数据处理,数据无重复的场景。通过用于判断某个数据在不在。
1.2位图的实现
#include<vector>
using namespace std;
namespace qyy
{
class BitSet//位图
{
public:
BitSet(size_t N)
{
_array.resize(N / 32 + 1, 0);//开空间并初始化为全0
int _num = 0;//位图中保存的数据个数
}
void set(int x)//将数据x加入位图
{
int index = x / 32;//求出x在第几个位置
int pos = x % 32;//求出是x是第几位
_array[index] |= (1 << pos);//将数据x加入到位图中
++_num;
}
void reset(int x)//将位图中的x取消
{
int index = x / 32;//求出x在第几个位置
int pos = x % 32;//求出是x是第几位
_array[index] &= ~(1 << pos);//将数据从位图中取出
--_num;
}
bool test(int x)//判断x是否在位图中
{
int index = x / 32;//求出x在位图中第几个位置
int pos = x % 32; //求出是x是第几位
return _array[index] & (1 << pos);//判断数据x是否在位图中
}
size_t Count()
{
return _num;
}
private:
vector<int> _array;
size_t _num;//保存位图中存储的数据个数
};
}
//Test.cpp--测试代码
void TestBitSet()
{
using namespace qyy;
BitSet bit(100);
bit.set(1);
bit.set(4);
bit.set(9);
bit.set(90);
bit.reset(90);
for (int i = 0; i < 100; ++i)
{
printf("[%d] : %d\n", i, bit.test(i));
}
}
问题: 如何在40亿个不重复的无符号整数中查找某个数是否存在 ?
解决方案:
1.遍历查找(时间复杂度O(N))
2.排序(O(N*logN)),然后利用二分查找
3.位图解决(由于数据量很大,且不重复所以位图是比较优的一种解决方案)
将所有的数据都存到位图中,如果想要查找某个数,只需要去位图中找一次就可以找到了,而且位图存储很节约空间。
void BitFind(int & x)
{
qyy::BitSet st(-1);//开无符号整数的最大数的数量个位
//加载数据并存放到位图中
//查找某个数据在不在位图中
st.test(x);
}
1.3位图的应用
1.快速查找某个数是否在一个集合中
2.排序+去重
3.求两个集合的交集和并集
4.操作系统中的磁盘块标记
2.布隆过滤器
2.1布隆过滤器的提出
我们平时在刷抖音的时候,它会不停地给我们推荐新的内容,每次它推荐的时候,都要对内容进行判断避免将已经推荐过的内容再次推荐给我们,那么问题来了,它是如何进行推荐的 ?用服务器记录用户看过的内容,当系统推荐新闻时会从用户的历史记录里面进行筛选,过滤掉那些已经存在的记录,那么如何进行快速查找呢?
方法:
1.用哈希表存储用户记录,缺点:浪费空间。
2.用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理了。
3.将位图和哈希表结合,即布隆过滤器。
2.2布隆过滤器的概念
布隆过滤器时由布隆在1970年提出的一种紧凑型,比较巧妙的概率型数据结构,特点是高效的插入和查询,可以告诉你“某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图中进行存储。此种方法不仅可以提升查询效率还可以节省大量的空间。
2.3布隆过滤器的插入
向布隆过滤器中插入一个字符串,首先这个字符串通过不同的 字符串哈希函数,可以找到不同的位置,然后将布隆过滤器中的这些位置都设置为1.
struct HashString1
{
//BKDRHash
size_t operator()(const string& str)
{
size_t hash = 0;
for (int i = 0; i < str.size(); ++i)
{
hash *= 131;
hash += str[i];
}
return hash;
}
};
struct HashString2
{
//SDBM Hash
size_t operator()(const string& str)
{
size_t hash = 0;
for (int i = 0; i < str.size(); ++i)
{
hash *= 65599;
hash += str[i];
}
return hash;
}
};
struct HashString3
{
//RS Hash
size_t operator()(const string& str)
{
size_t hash = 0;
size_t magic = 63689;
for (int i = 0; i < str.size(); ++i)
{
hash = hash * magic + str[i];
magic *= 378551;
}
return hash;
}
};
void set(const T& key)//将数据加入布隆过滤器
{
int index1 = Hash1()(key) % N;//避免超出位图的范围
int index2 = Hash2()(key) % N;
int index3 = Hash3()(key) % N;
_bs.set(index1);
_bs.set(index2);
_bs.set(index3);
}
2.4布隆过滤器的查找
布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射的位置的bit一定是1,所以可以按照一下的方式进行查找:分别计算每个哈希值对应的位图中的存贮位置是否为1 ,如果都为1的话,这个元素可能是存在的,如果有一个为0则说明这个元素一定不存在。
注意:布隆过滤器说某个元素不存在的时候说明这个元素一定不存在,但是如果布隆过滤器说某个元素存在,则说明这个元素可能存在,因为有些哈希函数存在一定的误判。
2.5布隆过滤器的删除
布隆过滤器不能直接删除元素,因为布隆过滤器是将一个元素映射到位图中的不同位置,所以可能关键字不同的元素映射到相同的位置,所以删除一个元素可能会影响到其它的元素。
一种支持删除的方法是:将布隆过滤器的每个比特位扩展为一个小的计数器,插入元素的时候给K个计算器(每次插入元素时,将对应的计数器加1),删除元素时给K个计算器-1,通过多占几倍存储空间的代价增加删除操作。
缺陷:
1.无法确认元素是否真正在布隆过滤器中
2.存在计数回绕
//布隆过滤器的实现代码
#include"BitSet.h"
#include<string>
namespace qyy
{
struct HashString1
{
//BKDRHash
size_t operator()(const string& str)
{
size_t hash = 0;
for (int i = 0; i < str.size(); ++i)
{
hash *= 131;
hash += str[i];
}
return hash;
}
};
struct HashString2
{
//SDBM Hash
size_t operator()(const string& str)
{
size_t hash = 0;
for (int i = 0; i < str.size(); ++i)
{
hash *= 65599;
hash += str[i];
}
return hash;
}
};
struct HashString3
{
//RS Hash
size_t operator()(const string& str)
{
size_t hash = 0;
size_t magic = 63689;
for (int i = 0; i < str.size(); ++i)
{
hash = hash * magic + str[i];
magic *= 378551;
}
return hash;
}
};
template<size_t N,size_t X = 5 ,class T = string,class Hash1 = HashString1,class Hash2 = HashString2,class Hash3 = HashString3>
class BloomFilter//布隆过滤器
{
public:
BloomFilter()
:_bs(N*X)
{}
void set(const T& key)//将数据加入布隆过滤器
{
int index1 = Hash1()(key) % N;//避免超出位图的范围
int index2 = Hash2()(key) % N;
int index3 = Hash3()(key) % N;
_bs.set(index1);
_bs.set(index2);
_bs.set(index3);
}
bool test(const T& key)//判断数据在不在布隆过滤器
{
int index1 = Hash1()(key) % N;//避免超出位图的范围
int index2 = Hash2()(key) % N;
int index3 = Hash3()(key) % N;
if (_bs.test(index1) == false)
return false;
if (_bs.test(index2) == false)
return false;
if (_bs.test(index3) == false)
return false;
return true;
}
//void reset(const T& key);//不支持删除,因为可能影响其它值
private:
qyy::BitSet _bs;
};
}
3.位图和布隆过滤器的优缺点
位图的优点:占用的空间少,在数据量很多的时候很好用,缺点:只能支持整形数据且对存在哈希冲突的数据不好处理。
布隆过滤器的优点:
1.增加和查询的时间复杂度为O(K)(K为哈希函数的个数,一般较小)。
2.哈希函数之间相互,没有关系,方便硬件并行运算。
3.布隆过滤器不需要存储数据本身对于一些保密要求严格的场所有很大的优势。
4.在能够承受一定的误判的情况下,布隆过滤器比其他数据结构有很大的空间优势。
5.数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
6.使用同一组散列函数的布隆过滤器可以进行交,并,差运算。
布隆过滤器的缺点:
1.存在一定的误判率,即假阳性,即不能准确的判断元素的是否在集合中(补救方法:再建一个白名单存储可能出现误判的数据)
2.不能获取元素本身。
3.一般情况下不能再布隆过滤器中删除元素。
4.如果采取计数方法删除,可能存在计数回绕问题。