位图及布隆过滤器的模拟实现与面试题
Posted 阿尔帕兹
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了位图及布隆过滤器的模拟实现与面试题相关的知识,希望对你有一定的参考价值。
位图
模拟实现
namespace yyq
template<size_t N>
class bitset
public:
bitset()
_bits.resize(N / 8 + 1, 0);
//_bits.resize((N >> 3) + 1, 0);
void set(size_t x)//将某位做标记
size_t i = x / 8; //第几个char对象
size_t j = x % 8; //这个char对象的第几个比特位
_bits[i] |= (1 << j); //标记
void reset(size_t x)//将某位去掉标记
size_t i = x / 8;
size_t j = x % 8;
_bits[i] &= (~(1 << j));
//测试值是否在
bool test(size_t x)
size_t i = x / 8;
size_t j = x % 8;
return _bits[i] & (1 << j);//整型提升,bool是4字节,char是1字节,按符号位来补
private:
std::vector<char> _bits;
;
当然位图也有缺点,它只能处理整型数据。
应用
- 快速查找某个数据是否在一个集合中
- 排序 + 去重
- 求两个集合的交集、并集等
- 操作系统中磁盘块标记
位图,是利用一个比特位来标识数据在不在(哈希的直接地址法),优点是节省空间,效率高,缺点是只能处理整型数据且要求数据相对集中。将哈希与位图结合,即布隆过滤器。
位图是要把一个数据通过一个哈希函数映射到一个位置,判断在不在;布隆过滤器是要把一个数据通过多个哈希函数映射到多个位置,降低误判率,判断一定不在或可能在
布隆过滤器
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
模拟实现
哈希函数个数的选择
哈希函数个数越多,布隆过滤器要开的bit位就越多,内存占用更大,则布隆过滤器bit位置为1的速度越快,但是效率变低;个数过少的话,误报率会变高。
k 为哈希函数个数,m 为布隆过滤器长度,n 为插入的元素个数,p 为误报率
计算公式为 k = m / n ∗ l n ( 2 ) k = m / n * ln(2) k=m/n∗ln(2)以及 m = − n ∗ l n ( p ) / l n 2 / l n 2 m = -n*ln(p) / ln2 / ln2 m=−n∗ln(p)/ln2/ln2
第一个公式可以得出 m = k ∗ n / l n 2 m = k * n / ln2 m=k∗n/ln2,当我们用3个哈希函数时,布隆过滤器的长度为 3 ∗ n / l n 2 ≈ 4.33 n 3*n/ln2 ≈ 4.33n 3∗n/ln2≈4.33n。
在代码中,我们直接取5n,代码中为X == 5,可以更改。
struct BKDRHashFunc
size_t operator()(const std::string& key)
size_t hash = 0;
for (auto ch : key)
hash *= 131;
hash += ch;
return hash;
;
struct APHashFunc
size_t operator()(const std::string& key)
size_t hash = 0;
const char* str = key.c_str();
for (int i = 0; *str; i++)
if ((i & 1) == 0)
hash ^= ((hash << 7) ^ (*str++) ^ (hash >> 3));
else
hash ^= (~(hash << 11) ^ (*str++) ^ (hash >> 5));
return hash;
;
struct DJBHashFunc
size_t operator()(const std::string& key)
size_t hash = 5381;
const char* str = key.c_str();
while (*str)
hash += (hash << 5) + (*str++);
return hash;
;
// N是最多存储的数据个数
// 平均存储一个值,开辟X个位
template<size_t N, size_t X = 5, class K = std::string, class HashFunc1 = BKDRHashFunc, class HashFunc2 = APHashFunc, class HashFunc3 = DJBHashFunc>
class BloomFilter
public:
void set(const K& key)
//3个哈希函数映射
size_t hashi1 = HashFunc1()(key) % (X * N);
size_t hashi2 = HashFunc2()(key) % (X * N);
size_t hashi3 = HashFunc3()(key) % (X * N);
_bs.set(hashi1);
_bs.set(hashi2);
_bs.set(hashi3);
bool test(const K& key)
//3个哈希函数映射
size_t hashi1 = HashFunc1()(key) % (X * N);
if (!_bs.test(hashi1))
//如果通过一个映射值不在,那肯定不在
return false;
size_t hashi2 = HashFunc2()(key) % (X * N);
if (!_bs.test(hashi1))
//如果通过一个映射值不在,那肯定不在
return false;
size_t hashi3 = HashFunc3()(key) % (X * N);
if (!_bs.test(hashi1))
//如果通过一个映射值不在,那肯定不在
return false;
//前三个映射值都存在,那么key可能在(有可能三个位置都冲突)
return true;
private:
std::bitset<N * X> _bs;
;
测试误判率
void test_bloomfilter2()
srand(time(0));
const size_t N = 100000;
BloomFilter<N> bf;
std::vector<std::string> v1;
std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";
for (size_t i = 0; i < N; ++i)
v1.push_back(url + std::to_string(i));
for (auto& str : v1)
bf.set(str);
// v2跟v1是相似字符串集,但是不一样
std::vector<std::string> v2;
for (size_t i = 0; i < N; ++i)
std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";
url += std::to_string(999999 + i);
v2.push_back(url);
size_t n2 = 0;
for (auto& str : v2)
if (bf.test(str))
++n2;
std::cout << "相似字符串误判率:" << (double)n2 / (double)N << std::endl;
// 不相似字符串集
std::vector<std::string> v3;
for (size_t i = 0; i < N; ++i)
std::string url = "zhihu.com";
url += std::to_string(i + rand());
v3.push_back(url);
size_t n3 = 0;
for (auto& str : v3)
if (bf.test(str))
++n3;
std::cout << "不相似字符串误判率:" << (double)n3 / (double)N << std::endl;
不支持reset
因为某一位可能被多个值映射,有冲突。把这个位reset掉,可能导致真的在的key就变成不在了。
面试题
1、给定100亿个整数,设计算法找到只出现一次的整数
位图要完成的事情是在不在,只需要2种状态==>1个比特位,char的8个比特位可以表示8个数的状态。而这道题需要3种状态(0:00
、1:01
、n:10
)==>2个比特位,char的8个比特位可以表示4个数的状态。
开两个位图,两个位图的相同的位置可以用0和1表示,当这个数出现第1次,第一个位图对应位置置1;第2次及以上次出现,第2个位图对应位置置1。
要筛选出现1次的整数,就用2个位图;要筛选出现2次的整数,就用3个位图,以此类推。
template<size_t N>
class twobitset
public:
void set(size_t x)//将某位做标记
if (!_bits1.test(x) && !_bits2.test(x))//00
_bits2.set(x);
else if (!_bits1.test(x) && _bits2.test(x))//01
_bits2.reset(x);
_bits1.set(x); //10
else//10
//啥也不做
private:
std::bitset<N> _bits1;
std::bitset<N> _bits2;
;
2、给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集
两个文件的话,每个文件分别使用一个位图,此时位图对应的功能就包括去重+交集。两个位图位置都为1,就是两个文件的交集。
3、位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数。
int的最大值为24亿多,找不超过两次的,要用到2个位图4种状态(00\\01\\10\\11),然后要过滤掉00和11这两个状态对应的数据
4、给一个超过100G大小的log文件, log中存着IP地址, 设计算法找到出现次数最多的IP地址?
ip是这样的127.0.0.1一个字符串。位图只能解决K问题(在不在),不能解决KV问题(多少次)。这里要求出现次数最多的,只能采用map来解决问题,100G大小肯定放不进去内存,我们利用哈希切割,先将文件分为100个小文件(注意不是平均分割),将每个小文件当作一个哈希桶,用函数将ip转成整型,i = HashFunc(ip) % 100
,i冲突的ip就会进入对应i号文件,那同一类ip就会进入同一个文件(相同的值一定会进入同一个文件,当然也会有哈希冲突的值),再对每个文件进行map统计出现次数。
如果:单个小文件超过1G,说明这个小文件里冲突的ip很多,a.大多是不同的ip/b.大多是相同的ip,该如何处理?
a.大多是不同的ip的情况,用map肯定无法完全统计,换个字符串哈希转换函数,递归再切分。
b.大多是相同的ip的情况,用map可以统计,大不了再用外排序。
如果map的insert失败,就表示没有内存了,相当于new节点失败,new失败会抛异常,就按a来处理。
5、给两个文件(A、B),分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法。
query是查询指令,比如可能是一个网页请求或者是一个数据库sql语句。
精确算法:假设每个query指令是50字节,那100亿个query大小约为500GB。将这些数据分到1000个小文件(Axx、Bxx),每个文件约0.5GB。每个小文件是通过同一个哈希函数,对应编号的文件里的数据大多是差不多的,把数据去个重,然后A01和B01分别用哈希表求交集,…A99和B99分别求交集。若小文件超过1GB,就再换个哈希函数再切分。
近似算法:用布隆过滤器,先把一个文件过一遍布隆过滤器,另一个文件来判断一下有哪些在。
6、如何扩展BloomFilter使得它支持删除元素的操作
计数器,有几个值映射到这个位,这个位就是几,当要求reset时,这个位置的值–。但是要实现计数的功能,映射位置就不能再使用一个位标记,而是需要多个位存储计数值,空间消耗成倍增加。故此方案在实际中不会被使用,还不如用哈希表。
哈希应用及海量数据面试题
文章目录
哈希的应用
位图
位图概念
-
面试题
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。
-
遍历,时间复杂度O(N)
-
排序(O(NlogN)),利用二分查找: logN
-
用set/unordered_set(底层是红黑树/哈希表)存起来再查找
以上方案的问题:数据量太大,放不到内存中
-
位图解决
数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比
特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在。比如:
-
-
位图概念
所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的
位图的实现
class bitset
private:
vector<int> _bs;
size_t _num;//总的位数
public:
bitset(size_t N)
//将所有位置为0
_bs.resize(N/32 + 1, 0);//N/32的只会得到商,余数被省略,还有部分位没有被开辟空间,所以结果加1,将余数补上
_num = N;
void set(size_t x)//将第x个数据的状态设置为1
//算出x在哪个数的哪一位上
size_t index = x / 32;//求出第x位在哪个数里面
size_t pos = x % 32;//求出第x位在该数的哪一位
//将该位设置为1
_bs[index] |= (1 << pos);
void reset(size_t x)//将第x个数据的状态设置为0
//同样先求出x在哪个数的哪一位上
size_t index = x / 32;//求出第x位在哪个数里面
size_t pos = x % 32;//求出第x位在该数的哪一位
//将该位设置为0
_bs[index] &= ~(1 << pos);
bool test(size_t x)//检查第x位的状态
//求出x在哪个数的哪一位
size_t index = x / 32;
size_t pos = x % 32;
return _bs[index] & (1 << pos);
// 获取位图中比特位的总个数
size_t size()const
return _num;
// 位图中比特为1的个数
size_t Count()const
int bitCnttable[256] =
0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 1, 2, 2, 3, 2, 3, 3, 4, 2,
3, 3, 4, 3, 4, 4, 5, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3,
3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3,
4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4,
3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5,
6, 6, 7, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4,
4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5,
6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 2, 3, 3, 4, 3, 4, 4, 5,
3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 3,
4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 4, 5, 5, 6, 5, 6, 6, 7, 5, 6,
6, 7, 6, 7, 7, 8;
size_t size = _bs.size();
size_t count = 0;
for(size_t i = 0; i < size; ++i)
int value = _bs[i];
int j = 0;
while(j < sizeof(_bs[0]))
unsigned char c = value;
count += bitCntTable[c];
++j;
value >>= 8;
return count;
;
再谈面试题:将40亿个数据是否存在用位图标记出来,但实际要开42(2^32)亿个空间,可以用下面三种方法开:
- bitset(-1)。bitset的构造函数的参数是无符号的,所以-1实际上就对应2^32
- bitset(0xffffffff),用十六进制表示
- bitset(pow(2, 32) ),用pow函数
位图的应用
- 快速查找某个数据是否在一个集合中
- 排序+去重
- 求两个集合的交集、并集等
- 操作系统中磁盘块标记
位图的优缺点
优点:节省空间,效率高
缺点:只能处理整型
布隆过滤器
布隆过滤器提出
我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?
- 用哈希表存储用户记录,缺点:浪费空间
- 用位图存储用户记录,缺点:一般只能处理整型,如果内容编号是字符串,就无法处理了
- 将哈希与位图结合,即布隆过滤器
布隆过滤器概念
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高频地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间
如果只用一种哈希算法,可能导致不同的值映射到了同一个位上,这样就会导致误判(哈希冲突),即:可能该类新闻我没有看过,但通过算法计算得到的位置,可能已经被标记成了看过,这样就导致了误判。怎么解决呢?
可以一个数据对应多个位,当这些位都为1时,就表明该数据出现了。就像上图一样,同一个字符串(也可能是其他类型的数据)通过三种哈希算法得到三个位,将这三个位都设置成1。但这只能降低冲突,并不能根除冲突
布隆过滤器的插入
向布隆过滤器中插入:“tencent”
// 假设布隆过滤器中元素类型为K,每个元素对应5个哈希函数
template<class K, class KToInt1 = KeyToInt1, class KToInt2 = KeyToInt2,
class KToInt3 = KeyToInt3, class KToInt4 = KeyToInt4,
class KToInt5 = KeyToInt5>
class BloomFilter
public:
BloomFilter(size_t size) // 布隆过滤器中元素个数
: _bmp(5*size), _size(0)
bool Insert(const K& key)
size_t bitCount = _bmp.Size();
size_t index1 = KToInt1()(key)%bitCount;
size_t index2 = KToInt2()(key)%bitCount;
size_t index3 = KToInt3()(key)%bitCount;
size_t index4 = KToInt4()(key)%bitCount;
size_t index5 = KToInt5()(key)%bitCount;
_bmp.Set(index1);
_bmp.Set(index2);
_bmp.Set(index3);
_bmp.Set(index4);
_bmp.Set(index5);
_size++;
private:
bitset _bmp;
size_t _size; // 实际元素的个数
布隆过滤器的查找
布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特位一定为1。
所以可以按照以下方式进行查找:分别计算每个哈希值对应的比特位置存储的是否为零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中
bool IsInBloomFilter(const K& key)
size_t bitCount = _bmp.Size();
size_t index1 = KToInt1()(key)%bitCount;
if(!_bmp.Test(index1))
return false;
size_t index2 = KToInt2()(key)%bitCount;
if(!_bmp.Test(index2))
return false;
size_t index3 = KToInt3()(key)%bitCount;
if(!_bmp.Test(index3))
return false;
size_t index4 = KToInt4()(key)%bitCount;
if(!_bmp.Test(index4))
return false;
size_t index5 = KToInt5()(key)%bitCount;
if(!_bmp.Test(index5))
return false;
return true; // 有可能在
注意:布隆过滤器如果某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可能存在,因为有些哈希函数存在一定的误判。
比如:在布隆过滤器中查找"alibaba"时,假设3个哈希函数计算的哈希值为:1、3、7,刚好和其他元素的比特位重叠,此时布隆过滤器告诉该元素存在,但实该元素是不存在的
布隆过滤器实现(主要针对string类型)
namespace ysj
//BKDR算法
struct HashStr1
size_t operator()(const string& str)
size_t ret = 0;
for (int i = 0; i < str.size(); ++i)
ret = ret * 131 + str[i];
return ret;
;
//SDBM算法
struct HashStr2
size_t operator()(const string& str)
size_t hash = 0;
size_t i = 0;
while (size_t ch = (size_t)(str[i]))
hash = 65599 * hash + ch;
//hash = (size_t)ch + (hash << 6) + (hash << 16) - hash;
++i;
if (i == str.size())
break;
return hash;
;
//RS算法
struct HashStr3
size_t operator()(const string& str)
size_t hash = 0;
size_t magic = 63689;
for (size_t i = 0; i < str.size(); ++i)
hash = hash * magic + str[i];
magic *= 378551;
return hash;
;
//主要实现字符串类型的布隆过滤器
template<class T, class Hash1 = HashStr1, class Hash2 = HashStr2, class Hash3 = HashStr3>
class bloomfilter
private:
bitset _bs;
size_t N;//统计bit位的长度
public:
bloomfilter(size_t num)
:_bs(5*num)//再准确一点是4.3倍,效率最好,网上有大佬核实了
,N(5*num)
void set(const T& x)
//通过三种算法获取具体的bit位
size_t index1 = Hash1()(x) % N;
size_t index2 = Hash2()(x) % N;
size_t index3 = Hash3()(x) % N;
cout << index1 << " " << index2 << " " << index3 << endl;
//将三个位置都设置成一
_bs.set(index1);
_bs.set(index2);
_bs.set(index3);
bool test(const T& x)
//获取每个位置
size_t index1 = Hash1()(x) % N;
size_t index2 = Hash2()(x) % N;
size_t index3 = Hash3()(x) % N;
//只要有一个位置不对就表示该数据不存在
if (_bs.test(index1) == false)
return false;
if (_bs.test(index2) == false)
return false;
if (_bs.test(index3) == false)
return false;
//也不一定该数据是存在的,可能是别的数据的位与该数据的位重合了,导致了误判
//但数据不存在是可以肯定的
return true;
//reset:不能简单地将x对应的三个bit位清零,因为有可能将其他数据的位也清零了。所以布隆过滤器不支持删除
;
布隆过滤器删除
**布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。**因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。
一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。
缺陷:
- 无法确认元素是否真正在布隆过滤器中
- 存在计数回绕
布隆过滤器优点
- 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
- 哈希函数相互之间没有关系,方便硬件并行运算
- 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
- 在能够承受一定的误判时,布隆过滤器比其他数据结构有着很大的空间优势
- 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
- 使用同一组散列函数的布隆过滤器可以进行交、并、差运算
布隆过滤器缺陷
- 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白
名单,存储可能会误判的数据) - 不能获取元素本身
- 一般情况下不能从布隆过滤器中删除元素
- 如果采用计数方式删除,可能会存在计数回绕问题
海量数据面试题
哈希切割
给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?
分析:首先我们要对ip地址的出现次数进行统计,假设使用kv模型的map解决,但这里有100G数据,无法将他们同时都放到内存中。
解法:可以先创建1000个小文件A0,A1,A2…A999,读取IP地址,计算出 i = hashstr(IP) % 1000,i是多少,就把该IP放入对应编号的小文件中,这样相同的IP地址就放入了同一个小文件,方便进行统计。
然后一次将一个小文件加载进内存,用一个map<string, int> countMap统计出该小文件中所有IP出现的次数,再用一个键值对pair<ip, int> max来保存出现次数最多的ip地址。当前小文件遍历完后,clear掉countMap,再加载下一个小文件进入内存,以此类推,最后遍历得出出现次数最多的IP地址。
与上题条件相同,如何找到top K的IP?如何直接用Linux系统命令实现?
找到topK的IP:把max遍历改成大小为k的堆即可,遍历得出topK的IP。求出现次数最多的K个IP就用小堆,出现次数最少的K个IP就用大堆。
位图应用
-
给定100亿个整数,设计算法找到只出现一次的整数?
方案一:用两个位来表示一个数存在的次数:
00:0次01:1次
10或11:两次及以上
方案二:在方案一思路的基础上,我们使用两个位图,第一个位图保存第一位的数据,第二个位图保存第二位的数据。统计次数时,如果该数对应的bit位上:
-
第一个位图为0:
1.第二个位图为0,说明它还没有出现过,要把它设置成01,也就是把第二个位图的该位上设置
以上是关于位图及布隆过滤器的模拟实现与面试题的主要内容,如果未能解决你的问题,请参考以下文章
-