【c++】位图与布隆过滤器

一.位图

1.位图的概念

40 亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在
40 亿个数中。【腾讯】
刚开始许多同学能想到的方法有:
1. 遍历,时间复杂度 O(N)
2. 排序 (O(NlogN)) ,利用二分查找 : logN
这两种方法都有缺陷:40亿个整数,大概就是16GB。40亿个字节大概就是4GB。排序要用到数组,要开出16GB大的数组,排在数组里才能进行二分查找,但是这些数组在内存里放不下,所以排序都排不了。那只能放到磁盘上,那数据在磁盘上就不能用二分了,不支持下标,效率也慢。
回归题目的本质,现在就是想知道某个数在不在这堆数据中,现在关心的只是在与不在两种状态。
可以使用一 个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0
代表不存在。比如:
【c++】位图与布隆过滤器_第1张图片

 

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

2.位图的实现

位图核心的三个操作是setresettest

set是将数据对应的比特位置设为1,reset是将x对应的比特置0,test用来查看数据在不在位于结构里,存在返回1,不存在返回0。

set() 函数的实现原理:

【c++】位图与布隆过滤器_第2张图片

 1<

reset()函数的原理类似首先确定该数据对应的位图结构,让1左移j位让后取反 变为 1111 0111然后与上_bits[ i ] 结果是这一个数据对应的位图结构被置为0,而其他的位图结构不变。

test()函数

【c++】位图与布隆过滤器_第3张图片

 首先还是确定数据对应的位图结构中的位置,将1左移到对应位置,然后与上_bits[ i ] 如果数据是存在的那最终就返回的是1,否则就返回的是0。

3.位图代码

#pragma once
#include 
#include 
using namespace std;

namespace cyf
{
	template
	class bitset
	{
	public:
		bitset()
		{
			_bits.resize(N/8+1, 0);   // 这里最值得注意 得保证所有的数据都能存下 
		}                             // 10 个数据 10 /8 =1 但是会有两个数据没有对应的位图
                                      //所以 +1 最多浪费7个比特位  最少浪费一个比特位
		void set(size_t x) 
		{
			size_t i = x / 8;
			size_t j = x % 8;
			_bits[i] |= (1 << j);
		}

		void reset(size_t x)
		{
			size_t i = x / 8;
			size_t j = x % 8;
			_bits[i] &= (~(1 << j));
		}

		bool test(size_t x)
		{
			size_t i = x / 8;
			size_t j = x % 8;
			return _bits[i] & (1 << j);
		}
	private:
		vector _bits;
	};

	void test_bitset()
	{
		
		//bitset<-1> bs2;
		bitset<0xffffffff> bs2;
		bs2.set(10);
		bs2.set(20);
		bs2.set(30);
		cout << bs2.test(11) << endl;
		cout << bs2.test(20) << endl;
		cout << bs2.test(33) << endl;
		cout << bs2.test(52) << endl;
		cout << bs2.test(45) << endl << endl;
		bs2.reset(20);
		bs2.set(666);
		cout << bs2.test(10) << endl;
		cout << bs2.test(20) << endl;
		cout << bs2.test(3000) << endl;
		cout << bs2.test(666) << endl;
		cout << bs2.test(777) << endl;
	}
}

 4.位图结构的应用

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

100亿个数字找到只出现一次的整数,这是KV模型的统计次数,数字有三种状态:0次、1次、1次以上,。这三种状态需要用两个比特位就可以表示,分别位00代表0次,01代表1次,10代表1次以上既可以。我们可以采用两个位图来实现,复用上面所实现的位图即可解决问题

template
	class twobitset
	{
	public:
		void set(size_t x)
		{
			if (!_bs1.test(x) && !_bs2.test(x))//00
			{
				_bs2.set(x);//01
			}
			else if (!_bs1.test(x) && _bs2.test(x))//01
			{
				_bs1.set(x);
				_bs2.reset(x);//10
			}
             else
               {
			       //10不变
               }
		}
		void PrintOnce()
		{
			for (size_t i = 0; i < N; ++i)
			{
				if (!_bs1.test(i) && _bs2.test(i))
				{
					cout << i << endl;
				}
			}
			cout << endl;
		}
	private:
		bitset _bs1;
		bitset _bs2;
	};

	void test_twobitset()
	{
		twobitset<100> tbs;
		int a[] = { 2,3,4,56,99,55,3,3,2,2,10 };
		for (auto e : a)
		{
			tbs.set(e);
		}
		tbs.PrintOnce();
	}

二.布隆过滤器

1.布隆过滤器的概念

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

位图只能针对整型,字符串通过哈希转化成整型,再去映射,对于整型没有冲突,因为整型是有限的,映射唯一的位置,但是对于字符串来说,字符串的数量是是无限的但是整形是有限的,就会发生冲突,会发生误判。这种情况查找一个字符串不在是准确的,因为字符串转换位整形后,对应的位图结构如果没有被标记为1,那就说明这个字符串不在,且一定是准确的。但是如果一个字符串的位图结构是被标记为1的,此时这个被标记的位图结构可能就是这个字符串,当然也有可能是别的字符串和这个字符串共用的一个位图结构,此时查找在的结果就是不准确的!!!

布隆过滤器:可以降低误判率:让一个值映射多个位置,但是并不是消除误判!

【c++】位图与布隆过滤器_第4张图片

 但是依旧可能存在误判: 当一个字符串与位图结构中的另一个字符串的对应的比特位正好是相等,所以在布隆过滤器只是降低了误判率并没有消除误判率。

如果布隆过滤器长度比较小,比特位很快会被占为1,误判率自然会上升,所以布隆过滤器的长度会影响误判率,理论上来说,如果一个值映射的位置越多,则误判的概率越小,但是并不是位置越多越好,空间也会消耗。所以误判率和空间大小也要有一个均衡值:

【c++】位图与布隆过滤器_第5张图片

 

k是哈希函数的个数,m是布隆过滤器的长度,n是插入元素的个数。K=3,ln2 取 0.7,那么 m 和 n 的关系大概是 m =4.2n ,也就是过滤器长度应该是插入元素个数的 4 -5倍

2.代码实现过滤器

#include
#include 

namespace cyf
{
	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;
		}
	};

	// 假设N是最多存储的数据个数
	// 平均存储一个值,开辟X个位
	template
	class BloomFilter
	{
	public:
		void set(const K& key)
		{
			size_t hash1 = HashFunc1()(key) % (N * X);
			size_t hash2 = HashFunc2()(key) % (N * X);
			size_t hash3 = HashFunc3()(key) % (N * X);
			size_t hash4 = HashFunc4()(key) % (N * X);

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

		bool test(const K& key)
		{
			size_t hash1 = HashFunc1()(key) % (N * X);
			if (!_bs.test(hash1))
			{
				return false;
			}

			size_t hash2 = HashFunc2()(key) % (N * X);
			if (!_bs.test(hash2))
			{
				return false;
			}

			size_t hash3 = HashFunc3()(key) % (N * X);
			if (!_bs.test(hash3))
			{
				return false;
			}

			size_t hash4 = HashFunc4()(key) % (N * X);
			if (!_bs.test(hash4))
			{
				return false;
			}

			// 前面判断不在都是准确,不存在误判
			return true; // 可能存在误判,映射几个位置都冲突,就会误判
		}

	private:
		std::bitset _bs;
	};

3.删除

布隆过滤器一般没有删除,因为布隆过滤器判断一个元素是会存在误判,此时无法保证要删除的元素在布隆过滤器中,如果此时将位图中对应的比特位清0,就会影响到其他元素了。这时候我们只需要在每个比特位加一个计数器,当存在插入操作时,在计数器里面进行 ++,删除后对该位置进行 -- 即可。但是布隆过滤器的本来目的就是为了提高效率和节省空间,在每个比特位增加额外的计数器,空间消耗那就更多了

4.布隆过滤器的优缺点

优点

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

缺点

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

你可能感兴趣的:(c++,算法,数据结构)