目录
一、位图
1.1 位图概念
1.2 位图的实现
1.3 位图的特点
二、布隆过滤器
2.1 布隆过滤器的提出
2.2 概念
2.3 实现原理
2.4 哈希函数个数和布隆过滤器长度的选择
2.5 布隆过滤器的删除
2.6 特点
2.6.1 优点
2.6.2 缺点
2.7 完整代码实现
三、海量数据处理常见面试题
位图就是用每一位来存放某种状态,适用于海量数据且数据无重复的场景。通常是用来判断某个数据是否存在的。
位图的底层结构依然是连续的线性空间,这里我们使用vector容器作为其底层结构。
#include
#include
using std::vector;
using std::cout;
using std::endl;
namespace bjy {
template
class bitset {
public:
bitset() {
if (N % 8 == 0) _bits.resize(N / 8, 0);
else _bits.resize(N / 8 + 1, 0);
}
void set(size_t which){//将某个位设置为1
size_t i = which / 8, j = which % 8;
_bits[i] |= (1 << j);
}
void reset(size_t which) {//将某个位设置为0
size_t i = which / 8, j = which % 8;
_bits[i] &= ~(1 << j);
}
bool test(size_t which)const {
size_t i = which / 8, j = which % 8;
return _bits[i] & (1 << j);
}
private:
vector _bits;
};
1. 速度快且节省时间。
2. 采用直接定址法,不存在哈希冲突。
3. 相对局限,只能处理整型数据。
4. STL中实现的bitset容器存在一定不足,其底层使用静态数组实现。其开辟在栈区会导致在处理海量数据时极易发生StackOverflow。(使用时应特别注意)
当使用新闻客户端看新闻时,其会不停地推荐新的内容,每次推荐时要去重。但新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何实现的呢?
方案一: 使用哈希表存储用户记录,但空间消耗过大。
方案二: 使用位图记录用户信息,明显不可取(位图只能处理整型数据)
方案三: 将位图与哈希进行结合,即布隆过滤器
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询。可以用来告诉你 "某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
Bloom Filters (jasondavies.com)https://www.jasondavies.com/bloomfilter/
布隆过滤器是一个 bit 向量或者说 bit 数组
若要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置进行set操作。例如针对key "百度" 用三个不同的哈希函数分别生成了哈希值 0、3、6,则上图转变为:
再存一个值 "腾讯",若哈希函数返回 2、3、7 的话,图继续变为:
3 这个bit位由于两个key的哈希函数都返回了3,因此它被覆盖了。
现在若想查询 "b站" 这个key是否存在,哈希函数返回了 0、4、7三个值,结果我们发现 4 这个 bit 位上的值为 0,说明没有任何一个key映射到这个 bit 位上,因此我们可以很确定地说 "b站"这个key不存在。而当我们需要查询 "百度" 这个值是否存在的话,那么哈希函数必然会返回 0、3、6,然后我们检查发现这三个 bit 位上的值均为 1,那么说明"baidu"可能存在。
为什么不是一定存在呢?因为随着增加的值越来越多,被置为 1 的 bit 位也会越来越多,这样某个key即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他key置为了 1 ,那么程序还是会判断这个key存在。
在使用布隆过滤器时,哈希函数的个数和布隆过滤长度的设定都是我们需要考虑的问题。
1. 哈希函数个数越多则布隆过滤器bit位被置为1的速度越快,且会效率下降。但是若哈希函数过少则会导致误报率较高。
2. 布隆过滤器的长度会直接影响误报率,布隆过滤器长度越长则误报率越低。但若是过长了则失去布隆过滤器原有的优势(占用空间小),反而还不如选择哈希表。
直接根据下图中的公式选择即可
布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。
一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计
数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储
空间的代价来增加删除操作。
缺陷:
1. 无法确认元素是否真正在布隆过滤器中
2. 存在计数回绕
1. 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
2. 哈希函数相互之间没有关系,方便硬件并行运算
3. 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
4. 在能够承受一定误判的情况下,布隆过滤器比其他数据结构有这很大的空间优势
5. 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
6. 使用同一组散列函数的布隆过滤器可以进行交、并、差运算
1. 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再
建立一个白名单,存储可能会误判的数据)
2. 不能获取元素本身
3. 一般情况下不能从布隆过滤器中删除元素
4. 如果采用计数方式删除,可能会存在计数回绕问题
#include
#include
#include
#include
using namespace std;
namespace bjy {
struct HashBKDR {
size_t operator()(const string& key) {
size_t sum = 0;
for (auto& e : key) {
sum = sum * 131 + e;
}
return sum;
}
};
struct HashAP {
size_t operator()(const string& key) {
size_t hash = 0;
for (size_t i = 0; i < key.size(); i++) {
if ((i & 1) == 0) {
hash ^= ((hash << 7) ^ key[i] ^ (hash >> 3));
}
else {
hash ^= (~((hash << 11) ^ key[i] ^ (hash >> 5)));
}
}
return hash;
}
};
struct HashDJB {
size_t operator()(const string& key) {
size_t hash = 5381;
for (auto ch : key) {
hash += (hash << 5) + ch;
}
return hash;
}
};
template
class Bloon_filter
{
public:
void set(const K& key) {
size_t pos1 = Hash1()(key) % (_ratio * N);
size_t pos2 = Hash2()(key) % (_ratio * N);
size_t pos3 = Hash3()(key) % (_ratio * N);
_bits->set(pos1);
_bits->set(pos2);
_bits->set(pos3);
}
bool test(const K& key) {
size_t pos1 = Hash1()(key) % (_ratio * N);
size_t pos2 = Hash2()(key) % (_ratio * N);
size_t pos3 = Hash3()(key) % (_ratio * N);
if (_bits->test(pos1) && _bits->test(pos2) && _bits->test(pos3)) return true;
else return false;
}
private:
const static size_t _ratio = 5;
bitset<_ratio* N>* _bits = new bitset<_ratio* N>;//库中bitset使用静态数组,开辟在栈上,容易导致栈溢出
};
}
利用两个位图完成该问题的实现。采用KV的统计次数的模型,两个位图中各有一个bit位表示同一个整型。00表示该整型出现次数为0,01表示该整型出现次数为1,10表示该整型出现次数为2次,11表示该整型出现3次及以上。
template
class twobitset
{
public:
void set(size_t x)
{
bool inset1 = _bs1.test(x);
bool inset2 = _bs2.test(x);
if (inset1 == false && inset2 == false)//00
{
_bs2.set(x);// -> 01
}
else if (inset1 == false && inset2 == true)//01
{
_bs1.set(x);
_bs2.reset(x);// ->10
}
else if (inset1 == true && inset2 == false)//10
{
// ->11
_bs1.set(x);
_bs2.set(x);
}
}
void print_once_num(){
for (size_t i = 0; i < N; ++i){
if (_bs1.test(i) == false && _bs2.test(i) == true){
cout << i << endl;
}
}
}
private:
bitset _bs1;
bitset _bs2;
};
将文件A中的数据映射到位图A中,将文件B中的数据映射到位图B中,位图A和位图B对应且都为1的位所代表的key的集合即是两个文件的交集。
该问题不可使用bitset解决,其只适用于整型数据。query为字符串,应使用哈希切割思想或者布隆过滤器完成问题。
近似算法:
使用两个布隆过滤器分别处理两个文件,若两个布隆过滤器set(key)都为1,即这个key在两个文件中都可能存在,近似看做交集中的元素。
精确算法:
假设1个query为30Byte,100亿个query则大概需要300G以上的内存空间。我们的电脑肯定没有这么大的空间,所以这里需要使用哈希切割的思想。
若是处理完后某个小文件过大,可以换一个哈希函数对小文件再次进行哈希切割。
问题一:
使用哈希切割,将一个大的log file分成500个小log file,相同的IP一定进入同一个小log file。再用map
问题二:
建立一个K个