位图与布隆过滤器 —— 海量数据处理

🌈 个人主页:Zfox_
🔥 系列专栏:C++从入门到精通

🚀 位图

一: 🔥 位图概念

🥝 所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。

💢 我们来看一道十分经典的面试题

给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。【腾讯】

  1. 遍历,时间复杂度O(N)
  2. 排序(O(NlogN)),利用二分查找: logN
  3. 位图解决
    数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在。
  • 位图的解法差不多是这道题的最优解,只需要将所有数据读入后将对应位置置1,然后再查找那个数据所储的位置是否为1即可。

二: 🔥 位图的实现思路及代码实现

🥝 位图的实现思路:

🎯 为了方便实现,位图的底层可以使用一个vector。而开空间并不根据数据的个数来开,而是根据数据的范围来开(如果开的空间不够,可能有位置无法映射到)。并且一个整型具有32个字节,所以如果我们要存N个数据,就只需要开N / 32 + 1的空间即可(+1是为了防止数据小于32和向上取整)。

🎯 当要操作一个数据时,先将其除以32来判断它应该处于数组中哪一个整型中。再对其%32,来判断它位于这个整型中的哪一个位上,此时再进行对应的位运算即可。

💢 代码实现及说明如下:

template<size_t N>
class bitset
{
public:
	bitset()
	{
		_bs.resize(N / 32 + 1);
	}

	// x映射的位标记成1
	void set(size_t x)
	{
		size_t i = x / 32;
		size_t j = x % 32;

		_bs[i] |= (1 << j);
	}

	// x映射的位标记成0
	void reset(size_t x)
	{
		size_t i = x / 32;
		size_t j = x % 32;

		_bs[i] &= (~(1 << j));
	}

	// x映射的位是1返回真
	// x映射的位是0返回假
	bool test(size_t x)
	{
		size_t i = x / 32;
		size_t j = x % 32;

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

private:
	std::vector<int> _bs;
};

三: 🔥 位图的应用

  • 💢 给定100亿个int,1G内存,设计算法找到只出现一次的整数。

首先,1G内存大约有80亿的bit位,而100亿个int,int 最多能表示大约42亿9千万个数,也就是说100亿的数据一半以上都是重复的;我们只用43亿个bit位就可以解决该问题,所以这里使用1G空间完全可以解决该问题。

这是一个KV统计搜索模型,我们可以使用两个位图来解决,用两个位图中对应位置的值来表示这个整数的出现情况:

0次 —> 00
1次 —> 01
2次及以上 —> 10

  • 🥝 我们可以复用上面我们自己实现的 bitset 去重新封装一个 twobitset

代码实现及说明如下:

template<size_t N>
class twobitset
{
public:
	void set(size_t x)
	{
		bool bit1 = _bs1.test(x);
		bool bit2 = _bs2.test(x);

		if (!bit1 && !bit2) // 00->01
		{
			_bs2.set(x);
		}
		else if (!bit1 && bit2) // 01->10
		{
			_bs1.set(x);
			_bs2.reset(x);
		}
		else if (bit1 && !bit2) // 10->11
		{
			_bs1.set(x);
			_bs2.set(x);
		}
	}

	// 返回0 出现0次数
	// 返回1 出现1次数
	// 返回2 出现2次数
	// 返回3 出现2次及以上
	int get_count(size_t x)
	{
		bool bit1 = _bs1.test(x);
		bool bit2 = _bs2.test(x);

		if (!bit1 && !bit2)
		{
			return 0;
		}
		else if (!bit1 && bit2)
		{
			return 1;
		}
		else if (bit1 && !bit2)
		{
			return 2;
		}
		else
		{
			return 3;
		}
	}

private:
	bitset<N> _bs1;
	bitset<N> _bs2;
};

🍊 这样我们就通过两个位图巧妙的解决了这个问题。

四: 🔥 STL中的 bitset

🎯 bitset官方文档

🍊 stl中的 bitset底层是一个静态数组,是在栈上开辟的空间,所以需要注意栈溢出的风险。

🍐 位图的优缺点:

优点:增删改查快、节省空间
缺点:只适用于整形

🚀 布隆过滤器

一: 🔥 布隆过滤器提出

🍊 我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?

1. 用哈希表存储用户记录,缺点:浪费空间。

2. 用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理了。
3. 将哈希与位图结合,即布隆过滤器。

二: 🔥 布隆过滤器概念

🍐 布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数将一个数据映射到位图结构中。此种方式 不仅可以提升查询效率,也可以节省大量的内存空间
在这里插入图片描述

🍐 布隆过滤器的思路就是把key先映射转成哈希整型值,再映射一个位,如果只映射一个位的话,冲突率会比较多,所以可以通过多个哈希函数映射多个位,降低冲突率。 布隆过滤器这里跟哈希表不一样,它无法解决哈希冲突的,因为他压根就不存储这个值,只标记映射的位。它的思路是尽可能降低哈希冲突。判断一个值key在是不准确的,判断一个值key不在是准确的。
在这里插入图片描述

三: 🔥 布隆过滤器的误判率推导

如果大家还想更深了解可以参考下面这篇文章
💢 如何选择哈希函数个数和布隆过滤器长度 一文中,对这个问题做了详细的研究和论证。

四: 🔥 布隆过滤器的实现

哈希函数
🍐 首先需要写几个哈希函数来将字符串转换成整形,各种字符串Hash函数一文中,介绍了多种字符串转换成整数的哈希函数,并且根据冲突概率进行了性能比较,有兴趣的朋友可以自行研究一下。

//下面三个字符串转换成整形的仿函数
struct HashFuncBKDR
{
	// @detail 本 算法由于在Brian Kernighan与Dennis Ritchie的《The CProgramming Language》
	// 一书被展示而得 名,是一种简单快捷的hash算法,也是Java目前采用的字符串的Hash算法累乘因子为31。
	size_t operator()(const std::string& s)
	{
		size_t hash = 0;
		for (auto ch : s)
		{
			hash *= 31;
			hash += ch;
		}
		return hash;
	}
};
 
struct HashFuncAP
{
	// 由Arash Partow发明的一种hash算法。  
	size_t operator()(const std::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
{
	// 由Daniel J. Bernstein教授发明的一种hash算法。 
	size_t operator()(const std::string& s)
	{
		size_t hash = 5381;
		for (auto ch : s)
		{
			hash = hash * 33 ^ ch;
		}
 
		return hash;
	}
};

🍊 布隆过滤器框架实现

template<size_t N,  //最多存储的数据个数。
	size_t X = 5, 
	class K = std::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;
 
		//cout << hash1 <<" "<< hash2 <<" "<< hash3 << endl;
 
		// 进行三次映射
		_bs.set(hash1);
		_bs.set(hash2);
		_bs.set(hash3);
	}
 
	// 判断每个比特位时,判断它不存在,注:不要判断它存在,因为不存在是准确的,存在是不准确的。
	bool Test(const K& key)
	{
		size_t hash1 = Hash1()(key) % M;
		if (!_bs.test(hash1))
		{
			return false;
		}
 
		size_t hash2 = Hash2()(key) % M;
		if (!_bs.test(hash2))
		{
			return false;
		}
 
		size_t hash3 = Hash3()(key) % M;
		if (!_bs.test(hash3))
		{
			return false;
		}
 
		return true; // 可能存在误判
	}
 
	// 获取公式计算出的误判率
	double getFalseProbability()
	{
		double p = pow((1.0 - pow(2.71, -3.0 / X)), 3.0);
 
		return p;
	}
 
private:
	static const size_t M = N * X;
	island::bitset<M> _bs;
};

五: 🔥 布隆过滤器的删除

  • 🎯 布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。
    在这里插入图片描述

“猪八戒” 和 “孙悟空” 映射的比特位都有第4个比特位。删除上图中 “猪八戒” 元素,如果直接将该元素所对应的二进制比特位置0,“孙悟空” 的元素也被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。

一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。
在这里插入图片描述

🇺🇳 缺陷:

1. 无法确认元素是否真正在布隆过滤器中
2. 如果采用计数方式删除,存在计数回绕

六: 🔥 布隆过滤器的应用

首先我们分析⼀下布隆过滤器的优缺点:

💢 优点

1. 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关。
2. 哈希函数相互之间没有关系,方便硬件并行运算。
3. 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势。
5. 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势。
5. 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能。
6. 使用同一组散列函数的布隆过滤器可以进行交、并、差运算。

💢 缺点

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

布隆过滤器在实际中的⼀些应用:

  • 爬虫系统URL去重

在爬虫系统中,为了避免重复爬取相同的URL,可以用布隆过滤器来进行URL去重。爬取到的URL可以通过布隆过滤器进行判断,已经存在的URL则可以直接忽略,避免重复的网络请求和数据处理。

  • 垃圾邮件过滤

在垃圾邮件过滤系统中,布隆过滤器可以用来判断邮件是否是垃圾邮件。系统可以将已知的垃圾邮件 的特征信息存储在布隆过滤器中,当新的邮件到达时,可以通过布隆过滤器快速判断是否为垃圾邮件,从而提高过滤的效率。

  • 预防缓存穿透

在分布式缓存系统中,布隆过滤器可以用来解决缓存穿透的问题。缓存穿透是指恶意用户请求⼀个不存在的数据,导致请求直接访问数据库,造成数据库压力过大。布隆过滤器可以先判断请求的数据是 否存在于布隆过滤器中,如果不存在,直接返回不存在,避免对数据库的无效查询。

  • 对数据库查询提效

在数据库中,布隆过滤器可以用来加速查询操作。例如:⼀个app要快速判断⼀个电话号码是否注册过,可以使⽤布隆过滤器来判断⼀个用户电话号码是否存在于表中,如果不存在,可以直接返回不存 在,避免对数据库进行无用的查询操作。如果在,再去数据库查询进行二次确认。

🚀 哈希切分

我们可以用哈希切分对海量数据处理问题

🔥 应用一

给两个⽂件,分别有100亿个query,我们只有1G内存,如何找到两个⽂件交集?

分析:假设平均每个query字符串50byte,100亿个query就是5000亿byte,约等于500G(1G约等于 10亿多Byte)

哈希表 / 红⿊树等数据结构肯定是⽆能为⼒的。

  • 解决方案1:

这个⾸先可以⽤布隆过滤器解决,⼀个文件中的query放进布隆过滤器,另⼀个文件依次查找,在的就是交集,问题就是到交集不够准确,因为在的值可能是误判的,但是交集⼀定被找到了。

  • 解决方案2:
  • 哈希切分首先内存的访问速度远大于硬盘,大文件放到内存搞不定,那么我们可以考虑切分为小文件,再放进内存处理。
  • 但是不要平均切分因为平均切分以后,每个小文件都需要依次暴力处理,效率还是太低了
  • 可以利⽤哈希切分依次读取文件中query,i=HashFunc(query)%N,N为准备切分多少分小文件,N取决于切成多少份,内存能放下,query放进第i号小文件,这样A和B中相同的query算出的 hash值i是⼀样的,相同的query就进⼊的编号相同的小文件就可以编号相同的文件直接找交集,不⽤交叉找,效率就提升了。
  • 本质是相同的query在哈希切分过程中,⼀定进⼊的同⼀个小文件Ai和Bi,不可能出现A中的的 query进⼊Ai,但是B中的相同query进⼊了和Bj的情况,所以对Ai和Bi进⾏求交集即可,不需要Ai 和Bj求交集。(本段表述中i和j是不同的整数)
  • 哈希切分的问题就是每个小文件不是均匀切分的,可能会导致某个小文件很⼤内存放不下。我们细细分析⼀下某个小文件很大有两种情况:
  1. 这个小文件中大部分是同⼀个query。
  2. 这个小文件是 有很多的不同query构成,本质是这些query冲突了。

针对情况1,其实放到内存的set中是可以放下的,因为set是去重的。针对情况2,需要换个哈希函数继续⼆次哈希切分。所以本体我们遇到大于1G小文件,可以继续读到set中找交集,若set insert时抛出了异常(set插⼊数据抛异常只可能是 申请内存失败了,不会有其他情况),那么就说明内存放不下是情况2,换个哈希函数进⾏二次哈希切分后再对应找交集。

在这里插入图片描述

🔥 应用二

给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?

本题的思路跟上题完全类似,依次读取文件A中query, i = HashFunc(query) % 500,query 放进 Ai 号小文件,然后依次⽤ map 对每个A小文件统计 ip 次数,同时求出现次数最多的 ip或者topk ip。本质是相同的 ip 在哈希切分过程中,⼀定进⼊的同⼀个小文件Ai,不可能出现同⼀个ip进⼊ Ai 和 Aj 的情况,所以对Ai进行统计次数就是准确的ip次数。

🚀 共勉

以上就是我对 位图与布隆过滤器 —— 海量数据处理 的理解,觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~😉
在这里插入图片描述

  • 141
    点赞
  • 121
    收藏
    觉得还不错? 一键收藏
  • 149
    评论
评论 149
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值