位图和布隆过滤器

一.位图

   我们先来看一道题:


   给定40亿个不重复的正整数,如何快速判断一个数组是否在这40亿个数中。

   以我们目前的思维,我们肯定是只能想到以下几种方法:

一    直接遍历,时间复杂度O(n)

二    二分查找,时间夫复杂度O(nlogn)
三    放入某种数据结构,如红黑树等等,时间复杂度最好O(logn)

但是无论是哪种方法,对空间的需求都是极大的, 40亿个整数 ≈ 14个g的内存  ,像我们平常电脑内存也就是16个 g或者说 32个g的内存,并且还有很多小内存电脑,可能很难应对如此大的空间需求。那么此时就可以引入位图。

1.1 位图的概念

  实际上,位图的本质也是一种数据结构。

  其作用是通过每一个bit位来标识每一种状态(其实也是哈希思想的一种体现),适合存储海量的数据,整数,数据无重复的场景, 一般是用来判断某一个数据是否存在的。

  比如上面那个例子,如果用每一个bit位来表示,一个整数原本是四个字节,总共需要4∗4000000000/1024/1024/1024,大约是占用了15G的内存,

而如果使用一个比特位标记这40亿个正整数存在不存在,4000000000*4/32/1024/1024,大约就是480M的内存。这个内存占用直接降低了一个数量级。

1.2 位图的原理  

  一个数据是否存在也就两种状态,在或者不在,通过二进制的0和1就可以表示。位图正是利用了这个特性,因为用一个bit位恰好可以表示这两种状态。 

  但因为我们在计算机语言中,无法直接拿取一个bit位,因此我们需要经过一些特殊的转换,进而拿到一个bit位。

1.3 位图的基本实现

  那么我们可以猜想,实际上位图的底层就是一个数组,但是我们需要通过一些特殊的转换,拿到数组里面的每一个bit位。 

  这里我们建议底层使用 char 数组,因为int类型会存在某些大小端问题,但是char 数组不会存在。

namespace BitMap {
	 
	template<size_t N>
	class BitMap {
		
	private:
		vector<char> _tables;
	};
}

  •  使用非类型模板参数,该参数用来指定位图比特位的个数
  •  底层使用的是vector,vector中是char类型变量。

1.4 位图的映射思想

  那么,我们该如何从一个char数组里面,准确的取到一个bit位呢?

  假设我们要插入一个9 我们要先确定它在数组中哪个char,然后再确定它在char 中哪一个位置。

  

我们可以发现 ,当9/8 时,我们能得到 9应该存在数组中的哪个位置,当9%8时,我们就可以得到9 在 char数组中的某个位置。

因此,我们得出:

  1. 求x在数组中的位置 :x/8
  2. 求x在char中的位置:x%8

1.5 位图的初始化

  我们都知道,位图采用了非类型模板参数,传入的N就代表了位图的大小,那么我们在初始化时,就应该规定好位图的大小。

  根据位图的映射思想,求出N在数组中的位置,也就是数组的大小,但我们需要把N+1 ,因为C++的除法思想,除数会小一个,因此,为了保险,需要把结果+1.

  

namespace BitMap {
	 
	template<size_t N>
	class BitMap {
	public:
		BitMap() {
			//初始化为0 代表没有这个元素
			_tables.resize((N >> 3) + 1, 0);
		}
	private:
		vector<char> _tables;
	};
}

1.6 位图的set函数 

 位图的set函数及其简单,我们根据位图的映射思想求出插入的数字需要映射到哪个位置,然后根据位运算直接将其映射到位图中。

	void set(size_t x)
	{
		//size_t i = x / 8;//映射到第几个char中
		size_t i = x >> 3;
		size_t j = x % 8;//映射到char中第几个比特位
		//将其映射到位图中
		_tables[i] |= (1 << j);
	}

示例,当我们插入9时,我们通过i求出它在哪一个 char中,然后通过j求出它在这个char中的哪一个比特位,同时按照由低到高的顺序,利用左移操作符(将一向高位移动),异或一下当前位置的值。

  从而把当前位置的0异或为1,我们不考虑重复问题(假设没有重复数字)

 1.7 位图的reset函数

  位图的reset(删除函数),基本思想上与set函数一致,但注意,这里不可以再使用异或操作符,而是采用在位运算中的清0操作。 

	//清零
	void reset(size_t x)
	{
	
		size_t i = x >> 3;
		size_t j = x % 8;
		//将比特位清0
		_tables[i] &= (~(1 << j));
	}

1.8 位图的test函数

  查找是否存在,这里和reset函数的思想基本一致。

//查找是否存在
bool test(size_t x)
{
	//映射到位图中的位置
	size_t i = x >> 3;
	size_t j = x % 8;

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

注意:这里的返回值涉及了一个整形提升问题,一个bool 是四个字节,从而产生了整形提升,故没有影响。

1.9 完整代码

template<size_t N>
class BitMap {
public:
	BitMap() {
		//初始化为0 代表没有这个元素
		_tables.resize((N >> 3) + 1, 0);
	}
	//置一
	void set(size_t x)
	{
		//size_t i = x / 8;//映射到第几个char中
		size_t i = x >> 3;
		size_t j = x % 8;//映射到char中第几个比特位
		//将其映射到位图中
		_tables[i] |= (1 << j);
	}

	//清零
	void reset(size_t x)
	{
	
		size_t i = x >> 3;
		size_t j = x % 8;
		//将比特位清0
		_tables[i] &= (~(1 << j));
	}

	//查找是否存在
	bool test(size_t x)
	{
		//映射到位图中的位置
		size_t i = x >> 3;
		size_t j = x % 8;

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

private:
	vector<char> _tables;
};

1.10 测试函数

void test1() {
	BitMap<10000> bt;

	bt.set(1);
	bt.set(7);
	bt.set(100);
	bt.set(2);
	
	cout << bt.test(1) << endl;
	cout << bt.test(7) << endl;
	cout << bt.test(100) << endl;
	cout << bt.test(2) << endl;

	cout << endl << endl;
	bt.reset(1);
	bt.reset(7);
	bt.reset(100);

	cout << bt.test(1) << endl;
	cout << bt.test(7) << endl;
	cout << bt.test(100) << endl;
	cout << bt.test(2) << endl;
}

代码结果为:

 当然,除了我们写的这三个函数之外,位图还有很多函数,具体可以看一下库里面的函数:
  https://cplusplus.com/reference/bitset/

1.11 位图的经典习题

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

  首先这个问题,以往我们学过的数据结构和算法都不行,只能用位图。

  但是我们也提到了,单个位图只能用来表示数字存在的问题,那么我们该如何做呢? 

  两个bit位能表示多少东西呢?0,1,2 显然是三个状态,因此,这个问题我们可以用两个位图来解决。

  完整代码::

#pragma once
#include"BitMap.h"
using namespace bitMap;
namespace dBitMap {

	template<size_t N>
	class DBitMap {
	public:
		
		//因为由两个位图,所以我们可以有三种状态
		// 不存在这个数时,两个位图中的这个位置都为0
		// 存在一次 bit1 为0 bit2 为1   也就是01
		// 存在两次  bit1 为1 bit2 为0   10
		// 其可以不用管 三次 但我们因为可以表示三种状态 因此  11为 三次
		// 三次以上 不用管,和本题无关,如果有要求,底层再加位图
 		void set(size_t x){
			//两个都不存在 变为01
			if (!bit1.test(x) && !bit2.test(x)) {
				bit2.set(x);
			}
			//01 变为 10
			else if (!bit1.test(x) && bit2.test(x)) {
				bit1.set(x);
				bit2.reset(x);
			}
			//10 变成11 
			else if (bit1.test(x) && !bit2.test(x)) {
				bit2.set(x);
			}
			//其它情况不处理
		}

		//这里我们设为一次只删除一个
		void reset(size_t x){

			//这里我们设为一次只删除一个
		
			//01 变为 00
			if (!bit1.test(x) && bit2.test(x)) {
				bit2.reset(x);
			}
			//10 变成01
			else if (bit1.test(x) && !bit2.test(x)) {
				bit2.set(x);
				bit1.reset(x);
			} 
			//11 变为10
			else {
				bit2.reset(x);
			}

			//想要一次全删除直接
		/*	bit1.reset(x);
			bit2.reset(x);*/
		}

		//查找是否存在
		bool test(size_t x){
			//也是分三种情况
				if (bit1.test(x) && bit2.test(x)) {
					cout << "出现三次" << endl;
				}
				else if (bit1.test(x) && !bit2.test(x)) {
					cout << "出现两次" << endl;
				}
				else if (!bit1.test(x) && bit2.test(x)) {
					cout << "出现一次" << endl;
					return true;
				}
				else {
					cout << "未出现" << endl;
				}
				return false;
		}

	private:
		BitMap<N> bit1;
		BitMap<N> bit2;
	};


	void test1() {

		DBitMap<100> DB;

		DB.set(1);
		DB.set(2);
		DB.set(3);
		DB.set(4);
		DB.set(5);
		DB.set(6);
		DB.set(1);
		

		DB.test(1);
		DB.test(2);
		DB.test(3);
		DB.test(4);
	}
}

代码结果:

1.12 位图的优劣 

优点:节省空间,效率高。
缺点:一般要求数据相对集中,否则会导致空间消耗上升。

位图的一个致命缺点:只能针对整形。

如果要位图存储整形之外的话,那么就必须像哈希函数一样,经过转化,但就拿字符串来说吧,即使经过转化,那么也可能存在不同的值,转化后变为相同的值这种情况,位图用一个bit位来标识这种情况,显然,更容易出现问题。

这时候就需要布隆过滤器了。

二.布隆过滤器

2.1 位图的概念

  布隆过滤器实际上就是位图的一种进阶形式,也是哈希思想的一种体现,其主要作用是检查字符串是否出现过。

  在布隆过滤器中,我们将字符串通过哈希函数转化为整形,然后插入到布隆过滤器中。

2.2 布隆过滤器的误判

  但是布隆过滤器也无法全部解决字符串误判问题,因为字符串实在是太大太大,太多太多了。

但是有以下两种情况:

位图中存在:不一定真正存在。
  因为可能有误判,那么他就不一定真正存在,针对这种情况,我们需要再进行详细的查找啊。

位图不存在:必然不存在。
  位图中本来就应该插入的位置没有元素,那就也没有其它误判的字符串,自己的字符串也没有,故肯定没有。

所以根据位图判断出的结构,不存在是准确的,存在是不准确的

有没有办法能提高一下判断的准确率呢?答案是有的,布隆过滤器就可以降低误判率,提高准确率。

2.3 布隆过滤器减少误判

  

布隆过滤器相比于位图有一个很重要的方法,它用多个哈希函数,将一个数据映射到位图结构中

  也就是说,我们不在单纯的用一个bit位标识这个数据是否存在,而是通过多个哈希函数,用多个bit位来标识这个数据是否存在。

只有一个字符串在位图中的几个比特位同时为1才能说明该字符串存在。

 

借用一下图片。

  但是此时只能减少误判,依旧不能避免误判。

2.4 布隆过滤器的应用场景

对数据不需要太准确的场景,比如注册昵称时的存在判断。

 如上图中,一个昵称的数据库是放在服务器中的,这个数据库中昵称的存在情况都放在了布隆过滤器中,当从客户端注册新的昵称时,可以通过布隆过滤器快速判断新昵称是否存在。

  这里的话,只要数据库中没有昵称,我们就可以创建,而布隆过滤器中,“没有”的判断场景是绝对正确的,因此,布隆过滤器完美适配这种情况,至于用户有没有取到好名字,那就不是我们用来关心的了。

2.5 哈希函数个数和布隆过滤器长度的关系

   现在知道布隆过滤器是什么了,但是我们到底该创建多少个比特位的位图(布隆过滤器长度),又应该使用多少个哈希函数来映射同一个字符串呢?

 如何选择哈希函数个数和布隆过滤器长度一文中,对这个问题做了详细的研究和论证:

  •  哈希函数个数和布隆过滤器长度以及误判率三者之间的关系曲线。

前面不管,后面不管,得出一个关系公式。

  • m:表示布隆过滤器长度。
  • k:表示哈希函数个数。
  • n:表示插入的元素个数。
  • 其中:ln2约等于0.69。

2.6 布隆过滤器的哈希函数

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

  这里我们选择四个哈希函数:

struct BKDRHash
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto ch : key)
		{
			hash *= 131;
			hash += ch;
		}
		return hash;
	}
};

struct APHash
{
	size_t operator()(const string& key)
	{
		unsigned int hash = 0;
		int i = 0;

		for (auto ch : key)
		{
			if ((i & 1) == 0)
			{
				hash ^= ((hash << 7) ^ (ch) ^ (hash >> 3));
			}
			else
			{
				hash ^= (~((hash << 11) ^ (ch) ^ (hash >> 5)));
			}

			++i;
		}

		return hash;
	}
};

struct DJBHash
{
	size_t operator()(const string& key)
	{
		unsigned int hash = 5381;

		for (auto ch : key)
		{
			hash += (hash << 5) + ch;
		}

		return hash;
	}
};

struct JSHash
{
	size_t operator()(const string& s)
	{
		size_t hash = 1315423911;
		for (auto ch : s)
		{
			hash ^= ((hash << 5) + ch + (hash >> 2));
		}
		return hash;
	}
};

2.7 布隆过滤器的框架

namespace BloomFliter
{
	template <size_t N,
			size_t X = 6,
			class K = string,
			class HashFunc1 = BKDRHash,
			class HashFunc2 = APHash,
			class HashFunc3 = DJBHash,
			class HashFunc4 = JSHash>
		class BloomFilter
		{
		public:
		private:
			std::bitset<X* N> _bs;
		};
}

  该模板有多个参数,但是大部分都是使用的缺省值,不用必须去传参,底层使用的STL库中的bitset。

size_t N:最多存储的数据个数。
size_t X:平均存储一个值,需要开辟X个位,这个值是根据上面的公式算出来的,此时哈希函数是4个,所以m = 4n/ln2 = 5.8n,取整后X位6,这里先给个缺省值是6。
class K:布隆过滤器处理的数据类型,默认情况下是string,也可以是其他类型。
哈希函数:将字符串或者其他类型转换成整形进行映射,给的缺省值是将字符串转换成整形的仿函数。

2.8 set 

	void set(const K& key) {

		//映射并插入四个位置
		size_t hashi1 = HashFunc1()(key) % (N * X);
		size_t hashi2 = HashFunc2()(key) % (N * X);
		size_t hashi3 = HashFunc3()(key) % (N * X);
		size_t hashi4 = HashFunc4()(key) % (N * X);

		_bset.set(hashi1);
		_bset.set(hashi2);
		_bset.set(hashi3);
		_bset.set(hashi4);
}

2.9 test()

  

bool test(const K& key) {
	//只要有一个位置不存在,就必定不存在,并且是准确的
	size_t hashi1 = HashFunc1()(key) % (N * X);

	if (!_bset.test(hashi1)){
		return false;
	}

	size_t hashi2 = HashFunc2()(key) % (N * X);
	if (!_bset.test(hashi2)) {
			return false;
	}
	size_t hashi3 = HashFunc3()(key) % (N * X);
	if (!_bset.test(hashi3)) {
			return false;
	}
	size_t hashi4 = HashFunc4()(key) % (N * X);

	if (!_bset.test(hashi4) ){
			return false;
	}

	return true;
}

2.10 测试代码

 

	void TestBF1()
	{
		BloomFilter<100> bf;
		bf.set("猪八戒");
		bf.set("沙悟净");
		bf.set("孙悟空");
		bf.set("二郎神");

		cout << bf.test("猪八戒") << endl;
		cout << bf.test("沙悟净") << endl;
		cout << bf.test("孙悟空") << endl;
		cout << bf.test("二郎神") << endl;
		cout << bf.test("二郎神1") << endl;
		cout << bf.test("二郎神2") << endl;
		cout << bf.test("二郎神 ") << endl;
		cout << bf.test("太白晶星") << endl;
	}

代码结果为:

2.11 布隆过滤器的优缺点 

  首先,布隆过滤器不能删除元素,因为可能会连带其它元素被删除

缺点:

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

优点:

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

  • 22
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
位图(Bitmap)和布隆过滤器(Bloom Filter)都是常用的数据结构,用于处理大规模数据集合,但它们有着不同的应用场景和用途。 位图是一种压缩数据结构,用于快速地判断某个元素是否在集合中。位图的实现方式是将每个元素映射到一个二进制位上,如果该元素存在于集合中,则将对应的二进制位标记为1,否则标记为0。这样,当需要查询某个元素是否在集合中时,只需要查找对应的二进制位即可。由于位图的实现方式非常简单,因此可以快速地进行插入和查询操作,而且占用的空间也非常小,适合处理大规模数据集合。 布隆过滤器也是一种快速判断元素是否存在于集合中的数据结构,但其实现方式与位图略有不同。布隆过滤器使用一组哈希函数将元素映射到多个二进制位上,并将对应的二进制位标记为1。当查询某个元素是否在集合中时,将该元素进行哈希映射,并查找对应的二进制位,如果所有的二进制位都被标记为1,则说明该元素可能存在于集合中,否则可以确定该元素不存在于集合中。布隆过滤器的优点是可以快速地判断一个元素不存在于集合中,而且占用的空间也比较小,但存在误判率的问题。 因此,位图布隆过滤器虽然都可以用来处理大规模数据集合,但它们的实现方式和应用场景有所不同。位图适用于需要快速地判断某个元素是否在集合中的场景,而布隆过滤器适用于需要快速地判断一个元素不存在于集合中的场景。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值