位运算+位图+布隆过滤器的简单梳理

位图是啥

  首先看一道题:

5e234188e05f4c7ea382a8df10104aeb.png

 首先像这种判断在不在的问题,我们第一想到的肯定是用set或者unordered_set。但是当数据量过大时,直接使用set这种就不适用了。40亿个整形的大小大概在16G左右,这样的话太大了。

  但是仅仅只是判断在不在的问题时,我们没必要如此大费周章,我们可以用第几位的比特位是0还是1来表示这个数存不存在,这样原本要16G的大小就变成0.5G。这就是位图的思想,跟位运算有着很大的关系。

位运算的复习

常用的位运算:

1. &:按位与

  如果同时为1则为1,其余情况都是0.

2. |: 按位或

  如果同时为0则为0,其余情况都是1.

3. ~:取反

  字面意思,1变成0,0变成1.

4. ^:异或

  相同就是0,不相同就是1.

运算技巧 

  一个int a。a^(a-1)可以求出最右端的一个1。

  现有int a,int b,a^b就是无进位相加。然后可以用(a&b)<<1,来获取最右端的一个进位。

深度解析异或

  异或有些规律非常好用:

假设有一个整形 a。

1.一个数异或它本身等于0,比如a^a=0(*消消乐*)(重要)。

2.一个数异或0等于它本身,比如a^0=a。

3.它满足交换律和结合律。

数的存储规则复习

  数字在计算机中是以补码的形式储存的,运算也是用的补码运算。

1.原码:

  就是我们对一个数转换成二进制。

2.反码:

  正码通过取反就是反码。

3.补码:

  反码加1就是补码。

这三种码注意转换顺序。注意:正数的原码,反码,补码均相同。

为什么计算机需要搞这么多码来储存呢?(补充)

  正数的相加减很容易想到并实现,但是对于负数该怎么办呢?计算机如何知道这个数是一个负呢?所以计算机将二进制的最高位不存储数据,将他作为符号位,0就说明该数是正数,1就说明该数是负数。

  现在判断正数还是负数搞定了,那么正数与负数相加怎么做呢?我们来举例1+(-1)=0.1的补码是…………(省略31个0)1,-1的补码为1…………(省略30个1)1。因为最高位不参与运算,所以它们相加时,由于溢出,除了符号位都变成了0。于是就解决了正数和负数相加的问题。

  那么现在还是可以来思考一个问题。1+(-1)后,符号位是0还是1呢?答案是0。毕竟0看起来好像可以用符号位为1的也可以用0的来表示,但是没有必要并且可能会有歧义。所以统一用符号位为0的来表示。那么1………………0不就空出来了?当然也不会让它闲着,于是它就是代表了INT_MIN。这就解释了为什么了int类型的大小范围是-2,147,483,648 到 2,147,483,647。当然扯这么多如果是无符号位(unsigned)那就没事了。

位图的实际运用

  所以我们知道了位图的基本思想就是通过用比特位来解决海量数据的问题。以下先来简单的模拟实现一个位图,再来看看具体样例。

位图的应用:

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

  先康康STL中位图的一些函数

c93aaf61203b4b0ca0f3d360b162dfae.png

代码模拟实现 

namespace hzj
{
	template<size_t N>
	class bitset
	{
	public:
		bitset()
		{
			_a.resize(N / 32 + 1);//开辟空间
		}
		//映射x位修改为1
		void set(const size_t& x)
		{
			int n1 = x / 32;
			int n2 = x % 32;
			_a[n1] |= (1 << n2);
		}
		//映射x位修改为0
		void reSet(const size_t& x)
		{
			int n1 = x / 32;
			int n2 = x % 32;
			_a[n1] &= (~(1 << n2));
		}
		//检查该位是否存在
		bool test(const size_t& x)
		{
			int n1 = x / 32;
			int n2 = x % 32;
			return _a[n1] & (1 << n2) ;
		}
	private:
		vector<int> _a;
	};
}

 可以看出位图的核心操作就是将值映射到比特位。

扩展示例:

  1.100亿个整数,如何找到只出现一次的数?

思路:

  对于每一个值,我们可以用两个比特位来表示。两个比特位一共有四种结果:00,01,10,11.那么我们可以用00表示不存在,01表示只出现了一次,大于01就说明出现了多次。

关于代码:

  如果我们还是用一个位图来做此题的话,在找映射位的时候就比较困难,用哈希的话来说就是容易出现哈希冲突,因此我们可以用两个位图来做。有了之前的核心操作,实现就简单了不少

大致思想图:

aef6b340645e45c389789f875af2efa4.png

  模拟实现如下:

template<size_t N>
	class twobitset
	{
	public:
		void set(const size_t& x)
		{
			//还没出现过,所以00->01
			if (!_b1.test(x) && !_b2.test(x))
			{
				_b1.set(x);
			}
			else if (_b1.set(x) && !_b2.set(x))//出现过一次了,01->10
			{
				_b1.reSet(x);
				_b2.set(x);
			}
			//出现超过了01了就不用在统计了
		}
		bool once(const size_t& x)
		{
			return _b1.test(x) && !_b2.test(x);
		}
	private:
		bitset<N> _b1;
		bitset<N> _b2;
	};

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

  关于这道题我们同样是使用两个位图,只需要把两个文件分别映射到两个位图中,然后把这两个位图对应的位置与一下,这样剩下的1就是两个文件的交集。

  1d097c35a353434e8c86e025caf6900a.png

3. 100亿个整数,求出现不超过两次的数?

大致思路与第一题一样,稍对代码进行改造就可以求出。

592e58313d3a4178ac2672bae0a9e4e2.png

布隆过滤器

布隆过滤器的提出

  我们知道整数可以通过比特位来进行一一映射而不冲突。然后如果是字符串这种,那么就有可能发生冲突。

  我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉
那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用
户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那
些已经存在的记录。 如何快速查找呢?
 来看看之前学过的容器:
1.哈希来存储用户记录:浪费空间。
2.用位图来存储用户记录:只能存整形,无法处理字符串。
3.哈希和位图结合->布隆模拟器

布隆过滤器的概念

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

 代码模拟:

struct BKDRHash
{
	size_t operator()(const string& s)
	{
		// BKDR
		size_t value = 0;
		for (auto ch : s)
		{
			value *= 31;
			value += ch;
		}
		return value;
	}
};
struct APHash
{
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (long 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 DJBHash
	
{
	size_t operator()(const string& s)
	{
		size_t hash = 5381;
		for (auto ch : s)
		{
			hash += (hash << 5) + ch;
		}
		return hash;
	}
};
template<size_t N,
	size_t X = 5,
	class K = string,
	class HashFunc1 = BKDRHash,
	class HashFunc2 = APHash,
	class HashFunc3 = DJBHash>
class BloomFilter
{
public:
	void Set(const K& key)
	{
		size_t len = X * N;
		size_t index1 = HashFunc1()(key) % len;
		size_t index2 = HashFunc2()(key) % len;
		size_t index3 = HashFunc3()(key) % len;
		/* cout << index1 << endl;
		cout << index2 << endl;
		cout << index3 << endl<<endl;*/
		_bs.set(index1);
		_bs.set(index2);
		_bs.set(index3);
	}
	bool Test(const K& key)
	{
		size_t len = X * N;
		size_t index1 = HashFunc1()(key) % len;
		if (_bs.test(index1) == false)
			return false;
		size_t index2 = HashFunc2()(key) % len;
		if (_bs.test(index2) == false)
			return false;
		size_t index3 = HashFunc3()(key) % len;
		if (_bs.test(index3) == false)
			return false;
		return true; // 存在误判的
	}
	// 不支持删除,删除可能会影响其他值。
	void Reset(const K& key);
private:
	hzj::bitset<X* N> _bs;
};

 布隆过滤器的查找

  布隆过滤器的思想是将一个元素用 多个哈希函数映射到一个位图中,因此被映射到的位置的比特
位一定为1。所以可以按照以下方式进行查找: 分别计算每个哈希值对应的比特位置存储的是否为
零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中
  注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可
能存在,因为有些哈希函数存在一定的误判。
  比如:在布隆过滤器中查找"alibaba"时,假设3个哈希函数计算的哈希值为:1、3、7,刚好和其
他元素的比特位重叠,此时布隆过滤器告诉该元素存在,但实该元素是不存在的。
  我们只能通过增加哈希函数和映射的比特位的位数来减少重叠率,无法避免。

布隆过滤器的删除

  布隆过滤器不能直接删除,因为删除是靠把比特位置成0来实现的,但是置成0的比特位又可能会影响到其他的数据。

  比如:删除上图中"tencent"元素,如果直接将该元素所对应的二进制比特位置0,“baidu”元素也
被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。
  一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计
数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用 几倍存储
空间的代价来增加删除操作。
缺陷:
 1. 无法确认元素是否真正在布隆过滤器中
 2. 存在计数回绕
 3. 如果依旧存在重叠的数,那么既没有删除也影响到了那个重叠的数。

布隆过滤器的优点

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

布隆过滤器的缺点

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

简单举一个应用场景(加深理解)

  平常我们打游戏的创建新账号的时候需要先创建一个昵称,作为游戏服务器后台,我们怎么知道用户上来取的昵称是否存在呢?

  如果我们用set,虽然效率还行,但是如果数据过于庞大,那么用户随便试一个名字可能都要等上几秒后才能知道该昵称是否存在,这个速度还是不太理想。仔细想想,如果我们对该昵称是否存在的判断不需要十分精确,那么此时布隆过滤器就可以上场了

7f5a7b003dde4f6a8563c8a49668c122.png

  这样使用布隆过滤器的话,对与存在的昵称就可以瞬间出结果,如果该昵称实际不存在,但是它显示存在也问题不是很大,反正又不会出现同名的用户就行。但是如果非要做出精确的判断的话,我们可以用两层数据结构,第一层用布隆过滤器,如果昵称不存在则第一层就能返回结果,第二层用set,第一层不能返回结果则再去第二层的set(数据库)中查找。

08fe0f7ba66d458ab32dccc6a428172c.png

 这样的话既能保证精确度的同时还能使效率有不错的提升,也更能理解“过滤器”这个词。

处理海量数据题目扩展

  有些问题用位图和布隆过滤器依旧不能很好的解决。大多都是跟字符串有关的

  这道题我们可以用哈希切分,比如将100亿个query(查询---可以理解位字符串)分成一千份的小文件,然后对每个文件进行编号,把每次的query通过哈希函数找到文件的编号,把它放进去,然后每次找交集的时候只需要先算出这个查询的编号,然后在这个编号的文件中查找就行了。

 但是中间会有一个问题,那就是当冲突太多时,一个文件的大小可能会过大,甚至超过我们的内存。此时会有两个场景:

1.存在很多重复的元素。

2.不是重复元素而是很多冲突的元素

  解决第一种我们可以先用set进行去重,如果只是有很多冲突的元素,超出了内存,那么set会抛出一个异常,此时我们可以换一个哈希函数,继续对这些元素进行哈希切分。如果是第二种,那么我们set会帮我们去重,也会减少很多重复元素的个数。

二 

同样我们可以使用哈希切分的思想,通过哈希函数把相同的IP地址存入同一个文件,然后再来统计次数就可以了。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值