W...Y的主页 😊
代码仓库分享💕
前言:我们使用hash思想学习了哈希表,进行了模拟实现unordered_set与unordered_map。这些都是用hash思想实现出来的数据结构,今天我们来学习一下hash的应用——位图、布隆过滤器。
目录
1. 哈希的应用
1.1 位图
1.1.1 位图概念
1.1.2 位图的实现
1.1.3 位图的应用
1.1.4变形应用
1.2 布隆过滤器
1.2.1 布隆过滤器提出
1.2.2布隆过滤器概念
1.2.3 布隆过滤器的插入
编辑
1.2.4 布隆过滤器的查找
1.2.5 布隆过滤器删除
1.2.6 布隆过滤器优点
1.2.7 布隆过滤器缺陷
1. 哈希的应用
1.1 位图
1.1.1 位图概念
1. 面试题
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在
这40亿个数中。【腾讯】
我们从脑海中第一冒出来的想法是什么呢?
1. 遍历,时间复杂度O(N)
2. 排序(O(NlogN)),利用二分查找: logN3.set+find函数
4. 位图解决前三种方法的唯一缺陷是40亿个数据太大,无法进行存储。(1G大概有10亿字节,10亿大概可以存储2.5亿个无符号整数,40亿大概需要16G内存。)
数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一
个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0
代表不存在。比如:
2. 位图概念
所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用
来判断某个数据存不存在的。
1.1.2 位图的实现
根据上述问题,我们需要开一个类数组的东西,需要开多大呢?无符号整数的范围是0~2^32-1,所以我们需要开2^32次方的比特位。大概需要512M的内存即可表式无符号整数是否存在。C++中提供位图bitset,我们自己进行模拟实现一下。
首先我们使用非类型模板参数,进行模板化。位图是存在数组中的,但是我们不能以比特位去开数组,所以我们可以使用bool、char、int进行定义。(以下代码是以int为单位开的数组,所以我们得除以32,向上取整+1)。
template<size_t N>
class bitset
{
public:
bitset()
{
_bits.resize(N / 32 + 1, 0);
//cout << N << endl;
}
// 把x映射的位标记成1
void set(size_t x)
{
assert(x <= N);
size_t i = x / 32;
size_t j = x % 32;
_bits[i] |= (1 << j);
}
// 把x映射的位标记成1
void reset(size_t x)
{
assert(x <= N);
size_t i = x / 32;
size_t j = x % 32;
_bits[i] &= ~(1 << j);
}
bool test(size_t x)
{
assert(x <= N);
size_t i = x / 32;
size_t j = x % 32;
return _bits[i] & (1 << j);
}
private:
vector<int> _bits;
};
当我们进行一一映射时,首先将数除以32来找到对应的哪个int中,在将此数模32算出在此int的具体位置,那个位置就是我们需要操作的地方。
set函数是将x数从0变1的函数,所以我们找到此位置后将1左移j位,然后进行或操作即可。
reset函数是将数从1变0的函数,所以我们先左移后再取反,最后进行与操作即可。
test函数是查找数是否出现过,我们直接返回左移j位再进行与操作的结果。
1.1.3 位图的应用
1. 快速查找某个数据是否在一个集合中
2. 排序 + 去重
3. 求两个集合的交集、并集等
4. 操作系统中磁盘块标记
1.1.4变形应用
1. 给定100亿个整数,设计算法找到只出现一次的整数?
使用map肯定是存不下这么多的数的,但是位图的功能是判断一个数是否出现。我们应该可以自己手撕一个位图进行,用两个字节存储信息:00代表没有出现过,01代表出现过一次,10代表出现两次以上。
我们也可以使用两个位图进行操作,和上面的逻辑一样,但是我们不用手撕,两个位图进行复用配合即可。
template<size_t N>
class two_bit_set
{
public:
void set(size_t x)
{
// 00 -> 01
if (_bs1.test(x) == false
&& _bs2.test(x) == false)
{
_bs2.set(x);
}
else if (_bs1.test(x) == false
&& _bs2.test(x) == true)
{
// 01 -> 10
_bs1.set(x);
_bs2.reset(x);
}
}
bool test(size_t x)
{
if (_bs1.test(x) == false
&& _bs2.test(x) == true)
{
return true;
}
return false;
}
private:
bitset<N> _bs1;
bitset<N> _bs2;
};
1.2 布隆过滤器
1.2.1 布隆过滤器提出
我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉
那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用
户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那
些已经存在的记录。 如何快速查找呢?
1. 用哈希表存储用户记录,缺点:浪费空间
2. 用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理
了。
3. 将哈希与位图结合,即布隆过滤器
1.2.2布隆过滤器概念
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概
率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存
在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也
可以节省大量的内存空间。
如何选择哈希函数个数和布隆过滤器长度https://zhuanlan.zhihu.com/p/43263751/
1.2.3 布隆过滤器的插入
向布隆过滤器中插入:"baidu"
作为字符串,我们必须将字符串先转换成整数再转换成映射位置进行存储,但是数组是无穷无尽的而整数是有限的,所以为了避免hash冲突,我们必须映射多组来减少哈希冲突。
首先我们得需要三个hash函数来将整数再转换成三个不同映射位置。字符串hash算法http://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html
struct HashFuncBKDR
{
// BKDR
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto ch : s)
{
hash *= 131;
hash += ch;
}
return hash;
}
};
struct HashFuncAP
{
// AP
size_t operator()(const string& s)
{
size_t hash = 0;
for (size_t i = 0; i < s.size(); i++)
{
if ((i & 1) == 0) // 偶数位字符
{
hash ^= ((hash << 7) ^ (s[i]) ^ (hash >> 3));
}
else // 奇数位字符
{
hash ^= (~((hash << 11) ^ (s[i]) ^ (hash >> 5)));
}
}
return hash;
}
};
struct HashFuncDJB
{
// DJB
size_t operator()(const string& s)
{
size_t hash = 5381;
for (auto ch : s)
{
hash = hash * 33 ^ ch;
}
return hash;
}
};
布隆过滤器:
template<size_t N,
class K = string,
class Hash1 = HashFuncBKDR,
class Hash2 = HashFuncAP,
class Hash3 = HashFuncDJB>
class BloomFilter
{
public:
void Set(const K& key)
{
size_t hash1 = Hash1()(key) % M;
size_t hash2 = Hash2()(key) % M;
size_t hash3 = Hash3()(key) % M;
_bs->set(hash1);
_bs->set(hash2);
_bs->set(hash3);
}
bool Test(const K& key)
{
size_t hash1 = Hash1()(key) % M;
if (_bs->test(hash1) == false)
return false;
size_t hash2 = Hash2()(key) % M;
if (_bs->test(hash2) == false)
return false;
size_t hash3 = Hash3()(key) % M;
if (_bs->test(hash3) == false)
return false;
return true; // 存在误判(有可能3个位都是跟别人冲突的,所以误判)
}
private:
static const size_t M = 10 * N;
std::bitset<M>* _bs = new std::bitset<M>;
};
1.2.4 布隆过滤器的查找
布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特
位一定为1。所以可以按照以下方式进行查找:分别计算每个哈希值对应的比特位置存储的是否为
零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中。
注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可
能存在,因为有些哈希函数存在一定的误判。
比如:在布隆过滤器中查找"alibaba"时,假设3个哈希函数计算的哈希值为:1、3、7,刚好和其
他元素的比特位重叠,此时布隆过滤器告诉该元素存在,但实该元素是不存在的。
1.2.5 布隆过滤器删除
布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。
比如:删除上图中"tencent"元素,如果直接将该元素所对应的二进制比特位置0,“baidu”元素也
被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。
一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计
数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储
空间的代价来增加删除操作。
缺陷:
1. 无法确认元素是否真正在布隆过滤器中
2. 存在计数回绕
1.2.6 布隆过滤器优点
1. 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无
关
2. 哈希函数相互之间没有关系,方便硬件并行运算
3. 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
4. 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
5. 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
6. 使用同一组散列函数的布隆过滤器可以进行交、并、差运算
1.2.7 布隆过滤器缺陷
1. 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再
建立一个白名单,存储可能会误判的数据)
2. 不能获取元素本身
3. 一般情况下不能从布隆过滤器中删除元素
4. 如果采用计数方式删除,可能会存在计数回绕问题
如何扩展BloomFilter使得它支持删除元素的操作?
我们可以结合计数来进行,如果一个位置有两个标记则记为2,删除时让计数-1即可,等到减到0时删除即可。