布隆过滤器
假如现在有40亿个ip地址(string类型),然后给你一个ip地址,让你查找这个ip地址在不在这40亿个ip地址里?我们应该怎么做呢?
- 如果用哈希表来处理的话,这里有40亿的数据,数据量太大,因此太占用空间
- 如果用位图来处理的话,这里因为是字符串,有可能不同的字符串映射的是同一个位,会有哈希冲突的问题,导致误判
- 因此我们采用的是哈希+位图的方法,也就是布隆过滤器
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。
布隆过滤器的优点:
- 查询效率高:使用了哈希的思想,一个数据映射了多个位
- 节省大量的内存空间:采用了位图
布隆过滤器的缺点:
布隆过滤器如果判断出该数据没有,那么这个数据一定没有,因为只要有一个映射的位不存在时,那么该数据就不存在。但是布隆过滤器如果判断出该数据有,那么其实是有可能出现误差的,因为字符串的计算难免会导致某两个字符串映射的几个位置都一样,因为我们写的哈希映射算法是固定的,字符串的随机排列组合是无限多的,所以如果两个字符串计算出来的映射的位一样时,就有可能导致误判。(也就是说这个字符串并不存在,但是由于映射的位和之前的字符串相同,所以就会判断出这个字符串也存在)
布隆过滤器的实现
布隆过滤器支持set接口,test接口,可以复用位图的set和test接口。但是布隆过滤器不能支持删除,因为通过多个哈希算法一个值会映射多个位,而且不同的值有可能会映射相同的位,如果删除的这个值所对应的位会变成0,那如果其他的值也映射这个位的话,就会导致其他的那个值被判定为不存在,但其实你并没有删除它。因此不支持删除。
那如果一定要实现删除操作呢?那就只能使用引用计数,但是采用引用计数删除的话就需要进行储存这个计数,需要额外的空间,会增大占用内存的大小。使用引用计数就是有多个值同时映射这个位,就把引用计数++,删除时就–,然后判断是否存在则是看值是否大于0。
具体代码如下:
BloomFilter.h
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include "bitset.h"
using namespace std;
template <class K, class HashFunc1, class HashFunc2, class HashFunc3>
class BloomFilter
{
public:
BloomFilter(size_t keynum)//开多少位呢?
:_bs(keynum * 5)
{}
void set(const K& key)
{
//如果是字符串,假如开了50个位,那么你映射的位不一定就在这50个位之间,因此要模上bitnum
//使计算出来的位都在这50个位里进行映射
size_t index1 = HashFunc1()(key) % _bs.GetBitNum();
size_t index2 = HashFunc2()(key) % _bs.GetBitNum();
size_t index3 = HashFunc3()(key) % _bs.GetBitNum();
_bs.set(index1);
_bs.set(index2);
_bs.set(index3);
}
//void reset(const K& key)//布隆过滤器不支持reset操作,因为同一个位可能被不同的字符串所映射
bool test(const K& key)
{
size_t index1 = HashFunc1()(key) % _bs.GetBitNum();
if (_bs.test(index1) == false)
return false;
size_t index2 = HashFunc2()(key) % _bs.GetBitNum();
if (_bs.test(index2) == false)
return false;
size_t index3 = HashFunc3()(key) % _bs.GetBitNum();
if (_bs.test(index3) == false)
return false;
return true;//有可能会存在误判的情况
}
private:
bitset _bs;//位图
};
//字符串哈希算法
struct StrHash1
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto e : s)
{
hash *= 131;
hash += e;
}
return hash;
}
};
struct StrHash2
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto e : s)
{
hash *= 65599;
hash += e;
}
return hash;
}
};
struct StrHash3
{
size_t operator()(const string& s)
{
size_t hash = 0;
size_t magic = 63689;
for (auto e : s)
{
hash *= magic;
hash += e;
magic *= 378551;
}
return hash;
}
};
void test_BloomFilter()
{
BloomFilter<string, StrHash1, StrHash2, StrHash3> bf(10);
cout << StrHash1()("https://mail.qq.com") << endl;
cout << StrHash2()("https://mail.qq.com") << endl;
cout << StrHash3()("https://mail.qq.com") << endl;
string url1("https://mail.qq.com1");
string url2("https://mail.qq.com2");
string url3("https://mail.qq.com3");
string url4("http://mail.qq.com");
string url5("https://maill.qq.com");
bf.set(url1);
bf.set(url2);
bf.set(url3);
cout << bf.test(url1) << endl;
cout << bf.test(url2) << endl;
cout << bf.test(url3) << endl;
cout << bf.test(url4) << endl;
cout << bf.test(url5) << endl;
}
bitset.h
#pragma once
#include <iostream>
#include <vector>
using namespace std;
class bitset
{
public:
bitset(size_t bitnum)//开多少位
:_bitnum(bitnum)
{
_bit.resize((bitnum >> 3) + 1, 0);//除8+1是确定数组开多大,数组中的每一个元素都可以标识8个数据
}
void set(size_t x)
{
//size_t index = x / 8;//计算是数组的第几个段
size_t index = x >> 3;
size_t num = x % 8;//计算是这个段的第几个位
//把对应的比特位置为1
_bit[index] |= (1 << num);
}
void reset(size_t x)
{
size_t index = x >> 3;
size_t num = x % 8;
//把对应的比特位置为0
_bit[index] &= (~(1 << num));
}
bool test(size_t x)
{
//判断对应位是0还是1
size_t index = x >> 3;
size_t num = x % 8;
return _bit[index] & (1 << num);//与完之后的结果如果非零就说明存在,为零说明不存在
}
size_t GetBitNum()
{
return _bitnum;
}
private:
vector<char> _bit;
size_t _bitnum;
};
test.cpp
#include "BloomFilter.h"
int main()
{
test_BloomFilter();
system("pause");
return 0;
}