16 哈希的应用---位图和布隆过滤器

文章目录


一、位图

1.1. 什么是位图

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

1.2. 位图的使用场景

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

如果数据量很小,比如不是40亿个而是40个,就可以采用直接遍历全部数据,进行查找的方式;也可以排序然后使用二分查找;或者用unordered_set容器进行查找。

但问题在于,40亿个无符号整数的总大小为40 * 4=160亿字节,而1G的空间是1024 * 1024 * 1024=1073741824字节≈10亿字节,也就是说160亿字节完全存储需要16G。在一般的电脑上面,是行不通的,因此我们可以用到位图的思想。

位图解决:

用40亿个比特位来标记所有的数字,1表示存在,0表示不存在。
无符号整数总共有232个,因此记录这些数字就需要232个比特位,也就是 232位 = 4G/8= 512M的内存空间,相比之下大大的节省了空间的消耗

在这里插入图片描述

1.3. 位图的应用

  1. 快速查找一个数据是否在一个集合之中
  2. 排序
  3. 求两个集合的交集、并集等等
  4. 操作系统中磁盘块的标记
  5. 内核中信号标志位(信号屏蔽字和未决信号集)。

优点:速度快,并且节省空间
缺点:只可以映射整形(如果有负数则开两个图,取反,映射到另外一个图)


二、位图的定义和成员函数

在这里插入图片描述

在定义位图时,需要传入位图的位的个数,也就是能表示的最大数据,比如传入16,则表示该位图有16位,可以表示数字0-15是否在位图中。

2.1. 定义方式

方式一: 构造一个16位的位图,所有位都初始化为0。

bitset<16> bs1; //0000000000000000

方式二: 构造一个16位的位图,根据所给值初始化位图的二进制位。

bitset<16> bs2(177);  //构造一个16位的bitset对象,将177转换为二进制,拷贝到内存空间

方式三: 构造一个16位的位图,根据字符串中的0/1序列初始化位图的前n位。

bitset<16> bs3("1110011");  //将二进制字符串初始化到对象中

在定义时需要注意以下几点

  • bitset与其它容器不一样,创建对象不需要提供容器变量类型,而是容器的大小,也即二进制位数
  • 如果bitset空间比二进制位数大,则在高位补0。
  • 在将整形常量转换为二进制过后,如果bitset位数不够,则自动放弃原数值高位的二进制位。
  • 将二进制字符串常量转换为二进制过后,如果bitset位数不够,则自动放弃原二进制字符串低有效位(也即右边的位)[这一点与整型常量有点不一样]
  • 在将整数二进制存储到bitset中,整数的低位对应bitset的低位,也即bitset[0]存储的原整数的二进制最低有效位。

2.2. 常用函数

成员函数功能
set设置指定位为1,如果不传参数则将所有位设为1
reset清空指定位或所有位
flip反转指定位或所有位
test测试某一位是否被置为1
count统计bitset里面1的位数
size获取bitset总的位数
any测试是否至少有1位被置为1(至少一个1,则返回true,否则返回false)
none测试是否没有一个被置为1(都是0,则返回true,否则false)
all测试是否都是1(都是1,返回ture,否则返回false)
to_string()以二进制字符串形式输出,将所有二进制位输出
to_ulong()转换为unsigned long整数,然后输出

2.3. 运算符的使用

bitset容器对>>、<<运算符进行了重载,我们可以直接使用>>、<<运算符对biset容器定义出来的对象进行输入输出操作。

同时赋值运算符:=,关系运算符:==、!=,复合赋值运算符:&=、|=、^=、<<=、>>=,单目运算符:~,位运算符&、|、 ^ 都进行了重载,可以对位图进行操作,其用法和操作整形的二进制位相同。

[ ]运算符也进行了重载,可以直接使用[ ]对指定位进行访问或修改。


三、位图的模拟实现

namespace hjl
{
	template<size_t N>//非类型模板参数
	class bitset
	{
	public:
		bitset()
		{
			//要多开一个整形,否则会不够,因为N/32是向下取整
			_bits.resize(N/32+1, 0);
		}
		//标记
		void set(size_t x)
		{
			assert(x < N);
			size_t index = x /32;//算出x映射的位在第几个整形
			size_t pos = x % 32;//算出x在这个整形中第几个位;

			_bits[index] |= (1 << pos);//将第index个整形中第pos个位置成1
		}

		//取消标记
		void reset(size_t x)
		{
			assert(x < N);
			size_t index = x / 32;//算出x映射的位在第几个整形
			size_t pos = x % 32;//算出x在这个整形中第几个位;
			_bits[index] &= ~(1 << pos);//将第index个整形中第pos个位置置成0
		}
		//反转位
		void flip(size_t x)
		{
			assert(x < N);

			//算出pos映射的位在第i个整数的第j个位
			int index = x / 32;
			int pos = x % 32;
			_bits[index] ^= (1 << pos); //将该进行反转(不影响其他位)
		}

		//查找在不在
		bool test(size_t x)
		{
			assert(x < N);
			size_t index = x / 32;
			size_t pos = x % 32;

			return _bits[index] & (1 << pos);
		}
		//获取被设置位的个数
		size_t count()
		{
			size_t count = 0;
			//将每个整数中1的个数累加起来
			for (auto e : _bits)
			{
				int num = e;
				//计算整数num中1的个数
				while (num)
				{
					num = num & (num - 1);
					count++;
				}
			}
			return count; //位图中1的个数,即被设置位的个数
		}
		//测试是否至少有1位被置为1(至少一个1,则返回true,否则返回false)
		bool any()
		{
			//遍历每个整数
			for (auto e : _bits)
			{
				if (e != 0) //该整数中有位被设置
					return true;
			}
			return false; //全部整数都是0,则没有位被设置过
		}
		//判断是否没有一个被置为1(都是0,则返回true,否则false)
		bool none()
		{
			return !any();
		}
		//测试是否都是1(都是1,返回ture,否则返回false)
		bool all()
		{
			size_t n = _bits.size();
			//先检查前n-1个整数
			for (size_t i = 0; i < n - 1; i++)
			{
				if (~_bits[i] != 0) //取反后不为全0,说明取反前不为全1
					return false;
			}
			//再检查最后一个整数的前N%32位
			for (size_t j = 0; j < N % 32; j++)
			{
				if ((_bits[n - 1] & (1 << j)) == 0) //该位未被设置
					return false;
			}
			return true;
		}
	private:
		vector<int> _bits;
	};
}

四、布隆过滤器

4.1. 布隆过滤器的提出

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

  1. 用哈希表存储用户记录,缺点:浪费空间
  2. 用位图存储用户记录,缺点:只可映射整形,就算将类型转换成整形,也无法处理哈希冲突
  3. 将哈希与位图结合,即布隆过滤器

4.2. 布隆过滤器的概念

布隆过滤器是由布隆在1970年提出来的,它的特点是比较高效的告诉你,某样东西一定不存在或者可能存在,它采用的方法是,用多个哈希函数,将一个数据映射到位图之中,这种方式不仅可以提高查询效率,还可以节省大量的空间。
在这里插入图片描述

布隆过滤器中一个值通过多个哈希函数,在位图中有多个映射位置,即使一个位置发生冲突了,还有另外的映射的值,降低了冲突的概率。由于映射多个位置,因此可能不同的值,处于同一个位置,虽然不能保证这个值一定存在,但是可以保证一个值一定不存在,因为只要有一个映射的位置为0,就说明该值不存在

4.3. 布隆过滤器的应用

布隆过滤器通常应用在允许误判的场景之中

  • 黑名单校验
    发现存在黑名单中的,就执行特定操作。比如:识别垃圾邮件,只要是邮箱在黑名单中的邮件,就识别为垃圾邮件。假设黑名单的数量是数以亿计的,存放起来就是非常耗费存储空间的,布隆过滤器则是一个较好的解决方案。把所有黑名单都放在布隆过滤器中,再收到邮件时,判断邮件地址是否在布隆过滤器中即可。

  • 身份验证:
    大门口的身份验证,如果不是小区里面的人,直接就拒绝进入(不在是确定的),如果通过了布隆过滤器的判断,再去数据库中对比一次,这样通过一层布隆过滤器可以提高这个查找系统的效率

4.4. 布隆过滤器的设计

1.选择合适的位图大小
在这里插入图片描述

2.插入
将每个哈希函数映射的位置都置为1

3.查找
所有的哈希函数映射的位置之中,只要有一个映射的位置为0,即当前值不存在,因为在插入的时候,所有的位置都设置为了1(所以不存在是准确的),否则表示存在(不准确,可能发生哈希冲突,是其它值映射的)

4.删除
布隆过滤器不支持删除工作, 因为不确定当前位置,是自己的,还是发生了哈希冲突其它的值映射过来的。
一种支持删除的方法:
将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。
但是这种方法也不好,因为计数器的大小不易确定,如果给小了,发生冲突会导致溢出(计数回绕,最大值-> 最小值)。如果给大了,浪费空间,脱离了布隆过滤器的本质思想。 所以一般的布隆过滤器不支持删除操作。

4.5. 布隆过滤器的优缺点

优点:

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

缺点:

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

五、布隆过滤器的代码实现

与位图不同,是库函数中是没有布隆过滤器的,需要手动实现。

namespace hjl
{
	class bitset
	{
	public:
		bitset(size_t N)
		{
			//要多开一个整形,否则会不够,因为N/32是向下取整
			_bits.resize(N / 32 + 1, 0);
		}
		//标记
		void set(size_t x)
		{

			size_t index = x / 32;//算出x映射的位在第几个整形
			size_t pos = x % 32;//算出x在这个整形中第几个位;

			_bits[index] |= (1 << pos);//将第index个整形中第pos个位置成1
		}

		//取消标记
		void reset(size_t x)
		{

			size_t index = x / 32;//算出x映射的位在第几个整形
			size_t pos = x % 32;//算出x在这个整形中第几个位;
			_bits[index] &= ~(1 << pos);//将第index个整形中第pos个位置置成0
		}

		//查找在不在
		bool test(size_t x)
		{

			size_t index = x / 32;
			size_t pos = x % 32;

			return _bits[index] & (1 << pos);
		}
	private:
		vector<int> _bits;

	};
	struct HashStr1
	{
		size_t operator()(const std::string& str)
		{
			size_t num = 0;
			for (auto& e : str)
			{
				num = num * 131 + e;
			}
			return num;
		}
	};
	struct HashStr2
	{
		size_t operator()(const string& str)
		{

			size_t num = 0;
			for (auto& e : str)
			{
				num = num * 65699 + e;
			}
			return num;
		}
	};
	struct HashStr3
	{
		size_t operator()(const std::string& str)
		{
			size_t num = 0;
			for (auto& e : str)
			{
				num = num * 7642 + e;
			}
			return num;
		}
	};
	template<class K, class Hash1= HashStr1, class Hash2= HashStr2, class Hash3=HashStr3 >//给定三个哈希函数
	class BloomFilter
	{
	public:
		BloomFilter(size_t num)
			//m(开的比特位数量) = k(哈希函数个数)*n(数据量)/ln2(0.7)
			:_count(5 * num)
			, _bitset(_count)
		{}

		void set(const K& key)
		{


			//获得哈希地址
			size_t pos1 = Hash1()(key);
			size_t pos2 = Hash2()(key);
			size_t pos3 = Hash3()(key);

			//将三个位置都设置为1
			_bitset.set(pos1 % _count);
			_bitset.set(pos2 % _count);
			_bitset.set(pos3 % _count);
		}

		bool test(const K& key)
		{
			//有一个为0,就是不存在的
			size_t pos1 = Hash1()(key);
			if (!_bitset.test(pos1 % _count))
				return false;

			size_t pos2 = Hash2()(key);
			if (!_bitset.test(pos2 % _count))
				return false;

			size_t pos3 = Hash3()(key);
			if (!_bitset.test(pos3 % _count))
				return false;

			//布隆过滤器判断在时不准确的,可能会存在误判
			// 判断不在是准确的
			//都为1,则true,不一定正确
			return true;
		}

	private:
		size_t  _count;
		bitset _bitset;
	};
}

六、海量数据常见处理方式(资源限制类题目)

在这里插入图片描述

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

分析:如果数据量很小,要统计次数,一般用kv模型的map就能解决。但是这里的问题是有100G数据,数据量太大放不进内存中。
此时采用哈希切割的方法,先创建1000个小文件A0-A999,读取IP计算出i=hashstr(IP)%1000,i是多少,IP就进入对应编号的Ai小文件。这样相同的IP一定进入了同一个小文件。然后使用map<string,int>countMap读取Ai中的IP统计出次数,一个文件读完,clear清空map,然后再读另一个文件,使用pair<string,int>max记录出现次数最多的IP即可。

在这里插入图片描述

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

判断一个值在不在,只需要两种状态,所以只使用一个位就可以。但是这里要找出值出现一次的数,其中有出现0次,出现2次及以上,此时有三种状态,说每个值使用两个位来表示就可以,出现0次用00表示,出现一次用01表示,出现两次及以上用10表示。然后遍历找出所有值为01的整数。
在这里插入图片描述

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

方法一:将其中一个文件1的整数映射到一个位图中,读取另外一个文件2中的整数,判断在不在位图中,在就是交集。此时消耗的内存是整数的范围232,也就是512M。
方法二:将文件1的整数映射到位图1中,将文件2的整数映射到位图2中,然后将两个位图中的数按位与,按位与之后为1的位就是交集,消耗内存1G。

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

与第二题类似,用两个位图进行存储,(0,0)表示出现0次,(1,0)表示出现1次,(0,1)表示出现两次,(1,1)表示出现多次

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

假设平均一个query是30-60字节,100亿个query大约占用300G-600G。

近似算法:将文件1中的query映射到一个布隆过滤器中,读取文件2中的query,判断在不在布隆过滤器中,在就是交集。
该方案存在缺陷:因为布隆过滤器判断不在是准确的,判断在是不准确的,可能存在误判。所以不会存在交集漏掉的情况,但是会导致交集中有不准确的数据。

分析思路:
这两个文件都非常大,也没有合适的数据结构能直接精确的找出交集,文件很大不能都放到内存中,那么我们可以把文件切分成多个小文件,将小文件的数据加载到内存中。该文件有300G-600G,此时切1000份,一个小文件为300M-600M,1G的内存可以搞定。
如果是平均切分,那么A0可以放到内存中存储到一个set中,然后用B0-B999与A0进行比较,接着A1放到内存中存储到set中,以此类推。可以看到这里的优势就是比较的过程放到内存中,且不是暴力比较,因为小文件Ax的数据是在set中,比较效率会高一些。但是这里需要不断的互相比较。
在这里插入图片描述

精确算法:哈希切割,不再平均切分,而是i=hashstr(query)%1000,i是多少,query就进入第Ai/Bi的小文件中,那么只需要比较Ai和Bi两个文件即可。因为两个文件的交集经过哈希算法以后,得到的i一定是相同,这样它们都会进入Ai和Bi。
在这里插入图片描述

但是哈希切割会出现一种情况,就是某个文件太大超过1G,这种情况可以考虑换个哈希算法,再切分一次。

6. 如何扩展BloomFilter使得它支持删除元素的操作

每个位标记成计数器,那么到底用几个为来表示计数器呢?给的位如果少了,那么多个值映射一个位置就会导致计数器溢出,比如1字节最多计数到256,假设有260个值都映射到一个位置,就会出问题。但是如果使用更多的位映射一个位置,那么空间消耗就大了。不符合布隆过滤器节省空间的特点。

7.多机存储、一致性哈希的问题。

在这里插入图片描述

在这里插入图片描述

8.如何保证哈西成环问题以后,机器能够均匀分布。

上面的一致性哈西解决了数据迁移问题,问题是哪台服务器映射哪些范围,如何保证他们映射的范围是均分的呢?

在这里插入图片描述

为了解决均分以及负载均衡的问题,我们可以采用虚拟节点技术
在这里插入图片描述

9.内存限制为3KB,但是只用找到一个没出现过的数

32位无符号整数的范围是0~4294967295(43亿),现在正好有一个包含40亿个无符号整数的文件。找到一个没出现过的数。
如果不限制内存或者内存限制为1G,只需要使用位图就能够解决(表示43亿需要232/8的字节,小于1G)。

但是这里限制内存为3KB。

使用分段统计的思想。
3KB最多能够申请长度为512的整形数组arr。我们可以把232分成512份,每份的长度为8388608,用数组统计落在该范围的长度,比如arr[0]表示0~8388608的数出现的次数,arr[1]表示8388609~8388609+8388608出现的次数。遍历40亿个数并统计,但是我们现在只有40亿个数,这就会导致arr中某个下标对应的数不够8388608,这就说明,这个区间有没有出现的数,假设为arr[1],然后我们就只需要将8388609~8388609+8388608这个小范围分成512分,重复上面的统计。
不断重复上述操作,就能够找出一个没出现过的数。

10.用有限几个变量,但是只用找到一个没出现过的数

条件和上个题类似。
此时可以采用二分策略,用L为0,R为232-1,mid为(L+R)/2,遍历40亿个数,然后我们统计L~midmid~R范围数出现的次数。必然有一侧次数是少于231的,然后我们继续采用二分的方式寻找这一侧。

11.有40亿个无符号整数,最多使用3KB内存,怎么找到这40亿个整数的中位数(上中位数)

32位无符号整数的范围是0~4294967295(43亿)。

和第9题类似,仍然使用分段统计的思想。继续分成512份。
一共40亿个数,我们需要找第20亿个数,假设arr[0]的值为1亿,arr[0]的值为5亿,arr[1]的值为15亿,因此中位数一定在arr[1]所对应的区间,所以我们只需要找arr[1]所对应的区间中,第14亿个数。接着再把arr[1]对应的范围分成512分,找第14一个数。
不断重复上述的操作。

12.32位无符号整数的范围是0~4294967295(43亿),有一个10G大小的文件,每一行都装着这种类型的数字,整个文件是无序的,给你5G的内存空间,请你输出一个10G大小的文件,就是原文件所有数字排序的结果

我们不需要5G空间,假设空间只够存3条记录(数+对应出现的次数),采用大根堆的策略,先遍历一遍文件,找到最小的3个数以及它们出现的次数并写回文件,找到最大值,假设这3个数中最大的值为11,然后清空记录,继续采用大根堆的方式,遍历文件,找大于11并且最小的三个数写回文件,找到最大值,不断重复上述操作。

13.求出大文件里出现字符串次数最多的前100名

和第一个题类似,采用分文件的思想,找出每个文件中字符串出现次数最多的前100名,然后把每个文件的前100名排序,找排序后的前100名即可。

也可以将每个文件中出现次数最多的前100名组成大根堆h1,h2,h3…hn,再把每个文件的大根堆的堆顶组成大根堆H,然后弹出H的堆顶,假设堆顶的数据来自h3,将h3堆顶的数据弹出,重新调整堆之后,将h3的堆顶的数据插入回H。
不断重复上述操作,知道凑够前100名。

资料参考:
哈希的应用
哈希(Hash)与加密(Encrypt)的基本原理、区别及工程应用

  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

今天也要写bug、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值