【C++】位图/布隆过滤器+海量数据处理

目录

一、位图

1.1 位图的概念

1.2 位图的实现

1.3 位图的应用(面试题)

二、布隆过滤器

2.1 布隆过滤器的引入

2.2 布隆过滤器概念

2.3 布隆过滤器的插入和查找

2.4 布隆过滤器的实现

2.5 布隆过滤器的优点和缺陷

2.6 布隆过滤器的应用(面试题)


一、位图

1.1 位图的概念

在C++中,位图(Bitmap)是一种数组,其中数组的每个元素可以表示多个布尔值。用于高效地存储和查询海量数据,其中元素的每个布尔值只占用一个位(bit)的空间。通常是用来判断某个数据存不存在的。

例如在32位系统中,一个unsigned int类型的变量可以表示32个布尔值。通过位操作,我们可以检查、设置或清除特定的位,从而实现对大量布尔值的快速访问和修改。

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

方法:

1. 遍历,时间复杂度O(N)
2. 排序(O(NlogN)),利用二分查找: logN
3. 位图解决

方法1和2中都需要将数据保存在数组中,40亿个整数需要16G内存,内存开辟不了这么大的连续空间。
数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一
个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0
代表不存在。
比如:

40亿个不重复的无符号整数的范围在0 ~ 2^32,有42亿多种可能性。一个字符型8个比特位,每个比特位来存储一个数字是否存在的状态,需要(2^32) / 8 = 2^29字节,即0.5G的内存。

1.2 位图的实现

C++标准库中的位图在线文档说明     #include <bitset>

在C++中,位图的实现通常使用unsigned int数组或者std::vector<unsigned int>。下面是一个简单的位图实现:

// N是需要多少比特位
template<size_t N>
class myBitSet
{
public:
	myBitSet()
	{
		_bs.resize((N >> 5) + 1, 0);//一个size_t 4个字节,32个比特位,右移五位相当于除以32,+1是防止有余数
		//注意:右移运算符>>优先级高于+,需要在右移时加上括号
	}

	void set(size_t x)//对第x个比特位置为1
	{
		size_t i = x / 32;//确定在哪一个size_t
		size_t j = x % 32;//确定在哪一个比特位上
		_bs[i] |= (1 << j);
	}

	void reset(size_t x)//第x个比特位置清0
	{
		size_t i = x / 32;//确定在哪一个size_t
		size_t j = x % 32;//确定在哪一个比特位上
		_bs[i] &= ~(1 << j);
	}

	bool test(size_t x)//返回第x位的状态
	{
		size_t i = x / 32;//确定在哪一个size_t
		size_t j = x % 32;//确定在哪一个比特位上
		return _bs[i] & (1 << j);
	}
	
private:
	vector<size_t> _bs;
};

1.3 位图的应用(面试题)

1. 快速查找某个数据是否在一个集合中
2. 排序 + 去重
3. 求两个集合的交集、并集等
4. 操作系统中磁盘块标记

1. 给定100亿个整数,设计算法找到只出现一次的整数?

100一个整数,它的范围也是只在0 ~ 2^32 之间,可以设计两个位图,每一次映射都调整位图里面的数字。例如出现0次设为00,出现1次设为01,二次及以上都为10。

template<size_t N>
class twobitset
{
public:
	void set(size_t x)
	{
		//00->01
		//01->10
		//10->不变
		if (_bs1.test(x) == false && _bs2.test(x) == false)//出现0次
		{
			_bs2.set(x);//新增设为01
		}
		else if (_bs1.test(x) == false && _bs2.test(x) == true)//出现1次
		{
			_bs1.set(x);
			_bs2.reset(x);//新增设为10
		}
		//出现2次及以上
	}

	void PrintOnce()
	{
		for (size_t i = 0; i < N; i++)
		{
			if (_bs1.test(i) == false && _bs2.test(i) == true)
			{
				cout << i << endl;
			}
		}
		cout << endl;
	}

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

2. 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?

各自映射到一个位图,若一个值在两个位图都存在,则是交集

3. 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数。

方法同问题1,创建两个位图,出现0次00,出现1次01,出现2次10,出现3次及以上 11。

二、布隆过滤器

2.1 布隆过滤器的引入

之前的搜索方法中,传统的数据结构和方法存在一定的局限性:

  • 暴力查找:数据量大了,效率就低。O(n)
  • 排序+二分查找:排序有代价、数组不方便增删。虽然查找的时间复杂度为 O(log n),但排序的时间复杂度为 O(n log n)
  • 搜索树 ->AVL树+红黑树:随着数据量的增加,树的高度也会增加,导致性能下降。
  • 哈希:当发生哈希碰撞时,性能会下降。此外,哈希表的空间消耗相对较高。
  • 以上数据结构,空间消耗很高。如何应对数量很大的数据?
  • 位图及变形:位图只能处理整数,对于其他类型的数据则不适用。

如果要应对其他类型数据的问题,如何快速查找?

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

2.2 布隆过滤器概念

布隆过滤器(Bloom Filter)是一种由布隆在1970年提出的概率型数据结构,它通过使用多个哈希函数将元素映射到一个 bit 数组中来实现高效的数据查询和插入。布隆过滤器的核心优势在于它的空间效率:它使用相对较少的内存来表示一个可能非常大的集合。可以用来告诉你 “某样东西 一定不存在 或者 可能存在”。

布隆过滤器的特点包括:

  • 高效插入和查询:插入和查询操作都非常快速,因为它们只涉及到哈希计算和位操作。
  • 概率性:布隆过滤器可能会返回误报(即一个元素被错误地判断为存在于集合中),但不会返回漏报(即如果一个元素被判断为不存在,那么它一定不存在)。
  • 固定大小:布隆过滤器的位数组大小在创建时确定,不会随着元素的添加而改变。
  • 不可删除:一旦元素被插入布隆过滤器,就无法删除,因为删除可能会导致其他元素的位被错误地设置为0。(传统布隆过滤器不可删除)

举个例子:原本在位图或哈希表/桶中,可能出现多个关键字映射到同一个位置,现在使用多个哈希函数,让原本映射到一个位置的结果映射到多个位置。检查关键字在不在,要把关键字映射的所有位置都检查一遍,只有所有位置的状态都为1,这个关键字才 “可能存在”,如果映射的位置有一个检测到了0,那么这个关键字肯定不存在。

详解布隆过滤器的原理,使用场景和注意事项

上面链接的讲解非常精彩,有兴趣的话可以看一下。

2.3 布隆过滤器的插入和查找

布隆过滤器是一个 bit 向量或者说 bit 数组

如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置为 1,例如针对值 “baidu” 和三个不同的哈希函数分别生成了哈希值 1、4、7,则上图转变为:

在再存一个值 “tencent”,如果哈希函数返回 3、4、8 的话,图继续变为:

值得注意的是,4 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位,因此它被覆盖了。现在我们如果想查询 “dianping” 这个值是否存在,哈希函数返回了 1、5、8三个值,结果我们发现 5 这个 bit 位上的值为 0,说明没有任何一个值映射到这个 bit 位上,因此我们可以很确定地说 “dianping” 这个值不存在。而当我们需要查询 “baidu” 这个值是否存在的话,那么哈希函数必然会返回 1、4、7,然后我们检查发现这三个 bit 位上的值均为 1,那么我们可以说 “baidu” 存在了么?答案是不可以,只能是 “baidu” 这个值可能存在。

这是为什么呢?答案跟简单,因为随着增加的值越来越多,被置为 1 的 bit 位也会越来越多,这样某个值 “taobao” 即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他值置位了 1 ,那么程序还是会判断 “taobao” 这个值存在。

2.4 布隆过滤器的实现

各种字符串Hash函数

选取3种平均分较高的哈希算法:BKDRHash,APHash,DJBHash。

#include "myBitSet.h"

struct BKDRHash
{
	size_t operator()(const string& key)
	{
		// BKDR
		size_t hash = 0;
		for (auto e : key)
		{
			hash *= 31;
			hash += e;
		}

		return hash;
	}
};

struct APHash
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (size_t i = 0; i < key.size(); i++)
		{
			char ch = key[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& key)
	{
		size_t hash = 5381;
		for (auto ch : key)
		{
			hash += (hash << 5) + ch;
		}
		return hash;
	}
};

template <size_t N, class K = string, 
	class HashFunc1 = BKDRHash, 
	class HashFunc2 = APHash, 
	class HashFunc3 = DJBHash>
class myBloomFilter
{
public:
	void set(const K& key)
	{
		size_t hash1 = HashFunc1()(key) % N;
		size_t hash2 = HashFunc2()(key) % N;
		size_t hash3 = HashFunc3()(key) % N;

		_bs.set(hash1);
		_bs.set(hash2);
		_bs.set(hash3);
	}

	// 一般不支持删除,删除一个值可能会影响其他值
	// 非要支持删除,也是可以的,用多个位标记一个值,存引用计数
	// 但是这样话,空间消耗的就变大了
	void reset(const K& key);

	bool test(const K& key)
	{
		//判断不存在是准确的
		size_t hash1 = HashFunc1()(key) % N;
		if (_bs.test(hash1) == false)
			return false;

		size_t hash2 = HashFunc2()(key) % N;
		if (_bs.test(hash2) == false)
			return false;

		size_t hash3 = HashFunc3()(key) % N;
		if (_bs.test(hash3) == false)
			return false;

		return true;//有可能误判
	}

private:
	myBitSet<N> _bs;
};

2.5 布隆过滤器的优点和缺陷

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

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

2.6 布隆过滤器的应用(面试题)

布隆过滤器使用场景:可以接受误判的场景

1. 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法。

query 是字符串,假设平均1个query是50byte,1G 约等于10亿byte,100亿个query就是5000亿byte ,约等于 500G,内存根本存不下。

500G的文件存放不下,可以将它们拆分成多个小文件,例如500个小文件a0~a499,每个文件大小就是1G。读取第一个文件的每个query,使用hash函数将query映射到500个文件中(计算 i = Hash(query) % 500),i 是几,query就进入第几个小文件。

两个文件中相同的query分别进入了编号相同的小文件 ai 和 bi。

两个大文件对应的编号相同的小文件ai 和 bi求交集,可以分别插入到一个setA和一个setB中,快速找到交集。

但是这样也可能有其他问题,例如某个文件可能太大,原因:

  1. 小文件中大多数都是某一个query,重复过多。
  2. 小文件有很多不同的query。

如果是情况1,文件很大有很多重复,后面重复播入都失败,可以插入到set,set可以去重。如果是情况2,不断插入set以后,内存不足,会抛异常,需要换一个哈希函数进行二次切分,再找交集。

近似算法(布隆过滤器)
1. 创建布隆过滤器:由于1G内存大约可以表示10亿字节,即约80亿bit,我们可以创建一个布隆过滤器,使用足够多的哈希函数(例如4到6个),以保持较低的误报率。

2. 插入第一个文件的query:遍历第一个文件中的每个query,对每个query应用哈希函数,并将对应位设置为1。

3. 查询第二个文件的query:遍历第二个文件中的每个query,同样应用哈希函数。如果布隆过滤器中的所有对应位都是1,则认为该query可能出现在第一个文件中。

4. 输出可能交集:将所有可能出现在交集中的query输出到新文件中。

5. 误报处理:由于布隆过滤器的性质,输出文件中可能包含一些误报,即实际上并不在第一个文件中的query。可以通过调整布隆过滤器的参数来减少误报率。

精确算法(哈希分割)
1文件分割:对两个文件的query进行哈希处理,然后使用除留余数法(%5000)将query分布到5000个不同的子文件中。每个子文件的大小大约为100M,这样每个文件的总大小就是5000 * 100M = 500G,符合原始数据的大小。

2.读取和比较:对于第二个文件中的每个query,同样进行哈希处理,然后将query放入对应的子文件中。接着,对于每个子文件,将其内容读取到内存中的unordered_map中,然后查找第一个文件中的对应子文件,看是否有相同的query。
3.输出交集:如果在一个子文件中找到了匹配的query,那么这个query就属于两个文件的交集。将这些query输出到新文件中。
4.内存使用:由于每个子文件是独立处理的,所以每次只需要将一个子文件的内容读取到内存中,这样内存的使用就被限制在了100M左右,符合1G内存的限制。
精确算法的关键在于将大文件分割成多个小文件,然后逐个处理,这样可以避免一次性将所有数据加载到内存中。这种方法虽然准确,但是处理时间可能会比近似算法长,因为需要多次读写磁盘。

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

与上一题同理,i= Hash(ip) % 100,这个ip就进入Ai小文件。相同的ip一定进入了同一个小文件,我们对小文件用map统计出ip次数就是准确的。

  • 22
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值