算法:位图与布隆过滤器

位图

什么是位图

  • 位图其实就是哈希的变形,同样通过映射到处理数据,只不过位图本身并不存储数据,而是存储标记
  • 通过一个比特位来标记这个数据是否存在,1代表存在,0代表不存在
  • 位图通常情况下用在数据量庞大,且数据不重复的情景下判断某个数据是否存在

位图的应用

  • 快速查找某个数据是否在一个集合中
  • 排序
  • 求两个集合的交集、并集等
  • 操作系统中磁盘块标记

快速查找某个数据是否在一个集合中

面试题:有1千万个无序不重复的整数,其范围在1到1亿之间。如何快速查找某个整数是否在这1千万个整数中呢?

分析:

  • 可以看到数据量比较大,这个时候就需要想内存是不是足够的问题
  • 同样的,数据量比较大,所以我们需要快速定位数据所在位置

思路:位图

  • 申请一个大小为1亿,数据类型为bool类型的数组。
  • 将这1千万个整数作为下标,将对应的数据设置为true。比如,整数5对应下标为5,那么就是arr[5] = true
  • 当我们要查询数 K K K是否存在是,只需要查询 a r r [ K ] arr[K] arr[K]是否为true即可,如果为true,表示存在,否则不存在。

但是,很多语言中提供的bool类型,大小是1byte的,并不能节省太多内存空间。实际上,表示true/false,我们只需要1bit就可以了。那么应该如何通过二进制比特位来表示true/false?

在这里插入图片描述

  • 可以借助位运算:
int main() {
    // 3200 bit  ---> bit[0~3199]
    std::vector<int> arr(100);  //int是32位的,100个int就有3200位,可以表示bit[0~3199]

    int pos = 453;
    //453位置设置为1
    arr[pos/32] = arr[453/32] | (1 << (453%32));
    //453位置设置为0
    arr[pos/32] = arr[453/32] & (0 << (453%32));
	// 提取状态
    int status = ((arr[453/32] >> (453 % 32)) & 1);
    printf("%d\r\n", status);

    return 0;
}
  • 所以实现为:
template<size_t N>
class bitset
{
public:
	bitset()
	{
		_bits.resize(N / 8 + 1,0);//开辟空间并置为0。对于开辟空间,一个char类型有8个位,所以需要个数/8即为需要开辟的大小,但是整数相除为向下取整,所以需要我们多开一个空间出来
		//_bits.resize((N >> 3) + 1,0); 
	}
	bool test(size_t x)
	{
		size_t i = x / 8;//处于的该数组的第几个空间
		size_t j = x % 8;//处于的该空间的第几个比特位

		return _bits[i] & (1 << j);
	}

	void set(size_t x)
	{
		size_t i = x / 8;//处于的该数组的第几个空间
		size_t j = x % 8;//处于的该空间的第几个比特位

		_bits[i] |= (1 << j);//该位置置为1
	}

	void reset(size_t x)
	{
		size_t i = x / 8;//处于的该数组的第几个空间
		size_t j = x % 8;//处于的该空间的第几个比特位

		_bits[i] &= (~(1 << j));//该位置置为0
	}
private:
	vector<char> _bits;  //对于底层来说一个位代表一个数的映射,那么我们以char类型来开辟对应需要空间,同时用vector进行管理
};

class bitmap{
public:
	bitmap(size_t N){
		_bits.resize(N / 32 + 1, 0); // 多开一个整型32bit
		_num = 0;
	}

	// 标记
	void set(size_t x){
		// 寻找x的标记存放在第几个整型
		size_t index = x / 32;
		// 寻找x的标记在这个整型的第几个位
		size_t pos = x % 32;

		//左移是向高位移动
		_bits[index] |= (1 << pos);
	}

	void reset(size_t x){
		// 寻找x的标记存放在第几个整型
		size_t index = x / 32;
		// 寻找x的标记在这个整型的第几个位
		size_t pos = x % 32;

		// 第pos个位置置为0
		_bits[index] &= ~(1 << pos);
	}

	// 判断x的映射位是否为1
	bool test(size_t x){
		// 寻找x的标记存放在第几个整型
		size_t index = x / 32;
		// 寻找x的标记在这个整型的第几个位
		size_t pos = x % 32;

		return _bits[index] & (1 << pos);
	}

private:
	std::vector<int> _bits;
	size_t _num; // 存储的数据个数
};

  • 当然,我们也可以用C++中提供的bitset来操作
    在这里插入图片描述

  • 注:使用成员函数set、reset、flip时,若指定了某一位则操作该位,若未指定位则操作所有位

#include <iostream>
#include <bitset>
using namespace std;

int main()
{
	bitset<8> bs;
	bs.set(2); //设置第2位
	bs.set(4); //设置第4位
	cout << bs << endl; //00010100
	bs.flip(); //反转所有位
	cout << bs << endl; //11101011
	cout << bs.count() << endl; //6
	cout << bs.test(3) << endl; //1
	bs.reset(0); //清空第0位
	cout << bs << endl; //11101010
	bs.flip(7); //反转第7位
	cout << bs << endl; //01101010
	bs.reset(); //清空所有位
	cout << bs.none() << endl; //1
	bs.set(); //设置所有位
	cout << bs.all() << endl; //1
	return 0;
}

延伸:

  • 整形数组最多申请的长度是21亿(INT_MAX),能表示的bit:21亿*32
  • 如果想要表示比long类型还要多的bit怎么办?用二维矩阵实现位图

问题:给定100亿个整数,设计算法找到只出现一次的整数。

1G大概是10亿个字节,100亿个整数就是400亿个字节,400亿个字节40G。

思路:

  • 这里的数字有三种状态:出现0次的、出现1次的,出现2次以及以上的
  • 出现0次的标志为00,出现1次的标志为01,出现2次及以上的标志为10。
  • 设置两个位图,位图1和位图2的对应位都提供一个为来标记数据,再遍历两个位图找出所有对应位映射为01的整数。

求两个集合的交集和并集

问题:给两个文件分别有100亿个整数,我们只有1G内存,如何找到两个文件的交集。

思路一:

  • 将其中一个文件中数据放到一个位图中,读取另一个文件中的整数,判断在不在位图中,如果在那就是交集,如果不在,那不是交集

思路二:

  • 将这两个文件分别映射到不同的位图中,在将这两个位图的对应位按位与,得到的位1就是交集

排序+去重

操作系统中的磁盘块标记

布隆过滤器

引入

问题:

  • 场景一:网页爬虫时有可能爬到相同的网页链接,我们应该如何避免这些重复的爬取呢?
  • 场景二:我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的?

思路:

  • 记录已经爬取的URL
  • 在爬取一个网页之前,先拿这个URL在已经爬取的网页链接中搜索
    • 如果存在,就不去爬
    • 如果不存在,就需要爬取
  • 当爬取完URL之后,将这个URL添加到已经爬取的网页链接链表

问题是:该如何记录已经爬取的网页链接呢?也即是用什么数据结构来记录呢?

分析,这个数据结构需要支持两个行为

  • 查找URL是否已经存在
  • 添加URL

因为这两个操作经常进行而且数据量会很大,所以这两个操作的执行效率应该尽可能高,而且内存占用应该尽量少。

我们知道散列表能支持快速定位数据。现在我们来看看它的内存消耗。

假设我们要爬取10亿个网页,为了判断重复,我们需要存储这10亿个URL,那么,它需要消耗多少数据呢?

  • 假设一个URL的平均长度是64字节,那单纯存储这10亿个URL,需要大约60GB的内存空间。因为散列表必须维持较小的装载因子,才能保证不会出现过多散列冲突。而且,用链表法解决总被的散列表,还会存储表指针。所以,如果将这10亿个URL构建成散列表,那需要的内存空间会远大于60GB,有可能超过100GB。
  • 当然,对于一个大型的搜索引擎来说,即便是100GB的内存要求,其实也不算太高,我们可以用分治的思想,用多台机器来存储这10亿数据

也就是说,哈希表:浪费存储空间
那能不能用位图

  • 因为这里数据范围是1~10 亿,所以需要 10 亿个二进制位
  • (注意,位图没有哈希冲突这一回事。10亿的数字10 亿个二进制位)

能不能在优化一下空间呢?用布隆过滤器布隆过滤器=哈希表与位图结合)。

  • 布隆过滤器对位图进行了一些优化,可以解决数据范围比较大的场景
  • 如果是布隆过滤器的话,假设这里用1亿的二进制位来表说10亿的值域,这时候我们就要用哈希函数来讲值域映射到二进制位上去。为了解决哈希冲突问题,布隆过滤器用多个位来表示一个值

具体操作如下

布隆过滤器

第一步:初始化一个长度为m比特的数组,每个bit位置0

那么n怎么确定呢?

第二步:准确k个hash函数

第三步:操作插入、查找

(1)插入某个数

  • 用K 个哈希函数,对同一个数字进行求哈希值,得到 K 个不同的哈希值,分别记作 X 1 , X 2 , X 3 , … , X n X_1 ,X_2 ,X_3 ,…, X_n X1X2X3Xn
  • 把这 K 个数字作为位图中的下标,将对应的 B i t M a p [ X 1 ] , B i t M a p [ X 2 ] , … , B i t M a p [ X n ] BitMap[X_1 ],BitMap[X_2 ],…,BitMap[ X_n] BitMap[X1]BitMap[X2]BitMap[Xn]都设置成 true
  • 也就是说,我们用 K 个二进制位,来表示一个数字的存在

(2)查询某个数是否存在

  • 用K 个哈希函数,对同一个数字进行求哈希值,得到 K 个不同的哈希值,分别记作 Y 1 , Y 2 , Y 3 , … , Y n Y_1 ,Y_2 ,Y_3 ,…, Y_n Y1Y2Y3Yn
  • 看这 K 个数字对应位图中的数值是否全部为true:
    • 如果全部为true,那么说明这个数字存在
    • 如果有任意一个数组不为true,那么说明不存在

在这里插入图片描述

对于两个不同的数字来说,经过一个哈希函数处理之后,可能会产生相同的哈希值。但是经过 K 个哈希函数处理之后,K 个哈希值都相同的概率就非常低了。尽管采用 K 个哈希函数之后,两个数字哈希冲突的概率降低了,但是,这种处理方式又带来了新的问题,那就是容易误判。
在这里插入图片描述

随着数据的不断插入,位图变红的部分越来越多,几乎全部变红,然后看谁谁都是嫌疑人,但是是嫌疑人的不一定是罪犯呀。也就是说误判有如下特点:只对存在误判。也就是:判定不存在一定不存在,判断存在有可能不存在

其误判率与哈希函数的个数、位图的大小有关

第四步:是否可以删除集合中的元素

一般情况下不支持删除。上面我们提到,在布隆过滤器算法中会存在一个bit位被多个元素值覆盖的情况,即bit位碰撞,如果我们删除元素时刚好重置该碰撞bit为0,那么其他元素在查找的时候,就会导致判断出错的问题发生。

如何设计一个布隆过滤器

  1. 先确定允不允许失误率,如果不允许,就不允许用布隆过滤器来做。

  2. 假设m为向量表的长度,k为哈希函数的个数,n为要插入的元素个数,p为误判概率。
    在这里插入图片描述

在实际应用中,我们一般是可以给定n、p,然后计算出m、k。

面试中:要求设计一个布隆过滤器

(1)了解现状

  • 先问面试官,样本量多少,假设为n
  • 然后问失误率多少,假设为p

(2)开始设计

  • 根据n和p,确定位图的大小m(向上取整)

在这里插入图片描述

  • 根据m和n,确定k值的选取(哈希函数的个数)(向上取整)
    在这里插入图片描述
  • 根据上面的m和k,修正出真实的p(只会比给出的小)

在这里插入图片描述
(3)k个哈希函数怎么得到

  • 其实只要两个哈希函数就可以加工出无限个哈希函数
    • 1 ∗ f 1 + f 2 1 * f_1 + f_2 1f1+f2
    • 2 ∗ f 1 + f 2 2 * f_1 + f_2 2f1+f2
    • 3 ∗ f 1 + f 2 3 * f_1 + f_2 3f1+f2

其他:失误率和m、k之间的关系:

  • (图一)随着m的增大,失误率会越来越低;
  • (图二)当m固定时,随着k的增大,失误率会先降低,然后再升高,所以,要注意把控k的取值范围;
    • 哈希函数的数量过少,会因为提取的特征不够多,影响失误率
    • 随着哈希函数过多,变红的位置就会变多,m就会快速的耗尽
      在这里插入图片描述

实现

template<class K = std::string, class Hash1 = HashStr1, class Hash2 = HashStr2, class Hash3 = HashStr3>
class bloomfilter{
public:
	bloomfilter(size_t num){
		_bs(-1);
	}
	void set(const K& key){
		// 通过多个哈希函数将数据映射到位图中
		size_t index1 = Hash1()(key);
		size_t index2 = Hash2()(key);
		size_t index3 = Hash3()(key);

		_bs.set(index1);
		_bs.set(index2);
		_bs.set(index3);
	}

	// 存在误删的问题
	void reset(const K& key){
		// 不支持删除
	}

	bool test(const K& key){
		// 判断在是不准确的,可能存在误判,判断不在是准确的。
		size_t index1 = Hash1()(key);
		if (_bs.test(index1) == false)
			return false;
		size_t index2 = Hash2()(key);
		if (_bs.test(index2) == false)
			return false;
		size_t index3 = Hash3()(key);
		if (_bs.test(index3) == false)
			return false;

		return true;
	}
private:
	// 底层其实是一个位图
	bitmap _bs;
	size_t len;
};

优缺点

优点:

  • 布隆过滤器允许有误判的情况下才可使用。
  • 哈希函数之间没有关系,方便硬件并行保存
  • 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
  • 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
  • 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能使用同一组散列函数的布隆过滤器可以进行交、并、差运算

缺点:

  • 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)
  • 不能获取元素本身
  • 一般情况下不能从布隆过滤器中删除元素
  • 如果采用计数方式删除,可能会存在计数回绕问题

应用(布隆过滤器允许有误判的情况下才可使用)

优点:占用内存少,插入和查询速度快,时间复杂度都为 O(k),与集合中元素的多少无关。

缺点:存在误判的情况,只能判断一个数据是否一定不存在, 无法判断一个数据是否一定存在。而且随着数据的增加,误判率会增加;另外数据无法删除。

优化:可以通过调整hash函数的个数、向量表长度与要存储数值个数之间的比例,降低误判率。

在这里插入图片描述

减少磁盘IO或者网络请求(一个值不存在,可以不用进行后续的查询请求)

比如说,在设计一个索引时

  • 因为布隆过滤器的内存占用小,而且判定不存在一定不存在,判断存在有可能不存在
  • 所以我们可以针对数据,构建一个布隆过滤器,并存储在内存中。当要查询数据的时候,先通过布隆过滤器,判断是否存在。如果其判断不存在,那么我们就没有必要去读取磁盘中的索引了

客户端内容推荐浏览内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值