文章目录
- 位图
- 概念
- 难点
- 代码
- 布隆过滤器
- 概念
- 插入
- 查找
- 删除
- 优缺点
- 代码
位图
概念
所谓位图,就是用每一个比特位位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。
- 遍历,时间复杂度O(N)
- 排序(O(NlogN)),利用二分查找: logN
- 位图解决
数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在。比如下图:
我们用 char 来实现位图,一个char类型的数据占8个字节,下标是 0 到 7 ,如下图 3 个 char 类型的数据,就可以表示 0 到 23 这些整数的状态(是否存在)。
此外,在每一个 char 类型的数据内部,我们可以选择右低左高 或者 左低右高,如下图,选择右低左高的方式,这就类似于大端存储。
难点
位图并不难理解,难点主要就是找到数据对应的比特位,然后将该比特位修改为 1 或者 0。
假设在位图 _b (用 char 实现)中寻找数据 x 对应的比特位分为两步:
- 找到位于哪一个 char 类型的数据中。 i = x / 8;
- 找到在该 char 类型的数据中,处于第几个位置。 j = x % 8;
这样子,我们就可以知道 x 在位图中的位于 _b[i] 的第 j 个位置。
- 如果要标记 x 存在,也就是把对应的比特位修改为 1。_b[i] |= (1 << j);
- 如果要标记 x 不存在,也就是把对应的比特位修改为 0。 _b[i] &= ~(1 << j);
1 << j ,代表着将 1 左移 j 位,然后 _b[i] 按位或 (1<<j) ,就可以把 _b[i] 的第 j 位设置为1。因为本文实现的位图,在每一个 char 类型数据中,是按照 左高右低,所以 1 左移 j 位。
标记 x 不存在,先将 1 左移 j 位,然后取反, 假设 j =3 , 那么 ~(1<<3) 的结果就是 11110111。用它和 _b[i] 按位与,由于第 j 位是 0,所以 _b[i] 必定是 0;其他七位是1,按位与之后 _b[i] 的其他七位保持原状。
代码
#pragma once
#include<vector>
#include<string>
using namespace std;
template<size_t N>
class bitset
{
public:
bitset()
{
_b.resize(N / 8 + 1, 0);
}
void set(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_b[i] |= (1 << j);
}
void reset(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_b[i] &= ~(1 << j);
}
bool test(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
return (1 << j) & _b[i];
}
private:
vector<char> _b;
};
布隆过滤器
概念
我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?
- 用哈希表存储用户记录,缺点:浪费空间。
- 用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理了。
- 将哈希与位图结合,即布隆过滤器。
布隆过滤器可以告诉我们 “某样东西一定不存在或者可能存在”,也就是说布隆过滤器说这个数不存在则一定不存,布隆过滤器说这个数存在可能不存在(误判,后面会讲到)。
其方法也不难理解,先将字符串进行哈希,映射到某个整数,然后用这个整数的状态(是否存在),标识这个字符串是否存在。( 整数的状态就可以用位图来表示。)
但是,为了保证正确性,一般会利用不同的哈希函数(正常是 3 个),分别对字符串进行哈希,得到多个不同的映射结果,用这多个结果共同标识字符串的存在与否(映射结果全部存在,字符串才存在,否则不存在)。
插入
如下,将字符串分别进行三次哈希映射,映射到三个整数,然后在位图中将这三个整数标记为存在(比特位修改为 1 ):
至于哈希函数,网络上有现成的,搜索即可。
查找
布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特位一定为1。所以可以按照以下方式进行查找:分别计算每个哈希值对应的比特位置存储的是否为零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中。
布隆过滤器的查找 会存在误判的情况,如下图,插入 x,y,z ,并没有插入 w,可是,当我们判断 w 是否存在时,三个哈希函数映射的结果都是 1,从位图的角度来看, w 就是存在的,但是实际上是因为 x,y,z 的哈希函数映射结果和 w 的三个重合了!
删除
布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。
比如:删除上图中"tencent"元素,如果直接将该元素所对应的二进制比特位置0,“baidu”元素也被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。
一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。
缺陷:
- 无法确认元素是否真正在布隆过滤器中
- 存在计数回绕
优缺点
布隆过滤器优点
- 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
- 哈希函数相互之间没有关系,方便硬件并行运算
- 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
- 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
- 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
- 使用同一组散列函数的布隆过滤器可以进行交、并、差运算
布隆过滤器缺陷
- 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)
- 不能获取元素本身
- 一般情况下不能从布隆过滤器中删除元素
- 如果采用计数方式删除,可能会存在计数回绕问题
代码
template<size_t N>
class bitset
{
public:
bitset()
{
_b.resize(N / 8 + 1, 0);
}
void set(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_b[i] |= (1 << j);
}
void reset(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_b[i] &= ~(1 << j);
}
bool test(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
return (1 << j) & _b[i];
}
private:
vector<char> _b;
};
#pragma once
#include<vector>
#include<string>
using namespace std;
struct BKDRHash
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto ch : s)
{
hash += ch;
hash *= 31;
}
return hash;
}
};
struct APHash
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (long i = 0; i < s.size(); i++)
{
size_t ch = s[i];
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ ch ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));
}
}
return hash;
}
};
struct DJBHash
{
size_t operator()(const string& s)
{
size_t hash = 5381;
for (auto ch : s)
{
hash += (hash << 5) + ch;
}
return hash;
}
};
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 len = N * _x;
size_t hash1 = Hash1()(key) % len;
_bs.set(hash1);
size_t hash2 = Hash2()(key) % len;
_bs.set(hash2);
size_t hash3 = Hash3()(key) % len;
_bs.set(hash3);
}
bool test(const K& key)
{
size_t len = N * _x;
// 只要有一个位置没有映射到,就是没有
size_t hash1 = Hash1()(key) % len;
if (!_bs.test(hash1))
{
return false;
}
size_t hash2 = Hash2()(key) % len;
if (!_bs.test(hash2))
{
return false;
}
size_t hash3 = Hash3()(key) % len;
if (!_bs.test(hash3))
{
return false;
}
return true;
}
private:
static const size_t _x = 5;
bitset<N * _x> _bs;
};