C++——位图、布隆过滤器

目录

1. 位图

1.1 位图模拟

1.2 运用

1.3 优缺点

2. 布隆过滤器

2.1 应用场景

2.2 模拟

2.3 运用


1. 位图

用例题来学习位图的使用场景:

40亿个不重复的无符号整数,如何快速判断某数是否存在。

  • 方案1:排序+二分
  • 方案2:红黑树
  • 方案3:哈希表

都不可以,整型(32比特位)能表示42亿多个整数。

40亿个整数,需要多少内存?40亿字节约等于4G,一个整数4字节,40亿个整数约16G。

一般的计算机,系统不可能给程序分配16G的内存。红黑树一个节点要存整数本身,颜色,左右子树,父的指针。哈希表比红黑树消耗少一点点,哈希表存数本身和next节点指针。只存16G的数据都存不下,更不用说还有附带消耗了。


  • 方案4:

这道题的需求是查找某值在还是不在,可以用一个比特位来标志某数的状态。0就是不存在,1就是存在。

使用哈希思想,将数据和地址映射。采用直接定址法,第0位表示整数0的状态,第1位表示整数1的状态....

总共需要2^32(约等于42亿)个比特位,就可以标志所有的整型。

  • 为什么不是开40亿个比特位?

因为这40亿个数都在整型范围内,最大的数可能是40亿,可能是42亿。开空间的个数不是看数据个数,而是看数据的范围。


1.1 位图模拟

数组可以是char数组,也可以是int数组。一个char能标志8个数,一个int标志32个数,除此之外没什么不同的。

#include<vector>
namespace my
{
	template<size_t N>
	class bitset
	{
	public:
		bitset()
		{
			_bits.resize(N/8+1,0);//+1是为了避免不能整除的时候,把空间开小了,可能造成越界访问问题。
		}
		void set(size_t x)//x对应位变成1
		{
			size_t i = x / 8;//第i个字节
			size_t j = x % 8;//x的哈希地址是第i个字节的第j位

			_bits[i] |=(1 << j);
		}

		void reset(size_t x)//x对应位变成0
		{
			size_t i = x / 8;
			size_t j = x % 8;

			_bits[i] &= (~(1 << j));
		}

		bool test(size_t x)//检查x是否存在
		{
			size_t i = x / 8;
			size_t j = x % 8;
			return _bits[i]  & (1<<j);//_bits不能改变。这种写法不可以:return (_bits[i]>>j) & 1
		}
	private:
		vector<char> _bits;
	};


	void test_bitset()//测试
	{
		//bit_set<100> bs1;
		//bit_set<-1> bs2;
		bitset<0xffffffff> bs3;

		bs3.set(0);
		bs3.set(888888);
		bs3.set(6);
		bs3.set(888);
		bs3.set(8888);
		bs3.set(953962);

		bs3.reset(6);

		cout << "6" << ":" << bs3.test(6) << endl;
		cout << "888888" << ":" << bs3.test(888888) << endl;
		cout << "888" << ":" << bs3.test(888) << endl;
		cout << "8888" << ":" << bs3.test(8888) << endl;
		cout << "953962" << ":" << bs3.test(953962) << endl;
		cout << "0" << ":" << bs3.test(0) << endl;

	}
}
  • 库中提供bitset容器

1.2 运用

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

需要三种状态,出现0次(00),1次(01),一次以上(10)。

  • 方法一:可以修改位图,原来用1个字节标志状态,现在用两个字节标志状态。set的时候,如果是00,变成01,如果是01变成10,如果是10就不变。最终遍历一遍,找出所有是01的位。
  • 方法二:用两个位图。
#include<bitset>
namespace twobitset
{
	template<size_t N>
	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);//10
				_bs2.reset(x);
			}
			//10 不变
		}

		void PrintOnce()
		{
			//01打印
			for (size_t i = 0; i < N; i++)
			{
				if (!_bs1.test(i) && _bs2.test(i))
				{
					cout << i << " ";
				}
			}
			cout << endl;
		}
	private:
		std::bitset<N> _bs1;
		std::bitset<N> _bs2;

	};


	void test()
	{
		twobitset<100> tbs;//所有数在100之内
		int a[] = { 3,5,6,10,10,10,15,20,21,33 };
		for (const auto& e  : a)
		{
			tbs.set(e);
		}
		
		tbs.PrintOnce();

	}
}
  • 100亿个数会不会存不下?

可以存下。因为位图开多大是由数的范围决定。100亿个数一定在整型范围内,会有重复的数。


 

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

找交集一定要去重。两个文件都放到位图里,test(x)都为1就是交集。

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

四种状态:00出现0次,01出现1次,10出现2次,11出现两次以上。

找出所有01和10的数。

哈希切分

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

不能用位图,因为要统计次数。文件过大,分批放到map里处理。

1.平均切分:100G,每10G分成一个小文件。每次取小文件统计次数,然后clear。再进下一个小文件。这样统计出的次数不是一个ip真正的次数,因为统计到只是当前小文件中出现的次数,其他文件里可能还有相同的ip。

2.哈希切分:i=HashFunc(ip)%100,i是多少就放进几号小文件。%100是为了保证每个小文件不超过1G。

ip相同的一定在同一个哈希桶里,这个桶里统计出的次数一定就是某ip的全部次数。

映射结束后,如果某一小文件的大小超过1G。可能是两种情况:

1.很多的相同ip。这种情况map可以统计。

2.很多的不同ip。map无法统计,更换哈希函数:用不同的算法将字符串转成整型和修改切割分数(%10或者模其他数都行)。

解决方法:超过1G的小文件,也用map统计,没报错就是情况1,出错了就捕获异常(情况2内存不够用,会insert失败),然后换一个哈希函数,递归再切分。

1.3 优缺点

  • 优点:节省空间,快。
  • 缺点:要求数据范围相对集中。如果非常分散,空间消耗变大。数据类型必须是整型。

string可以用位图存放吗?

不行。

  1. 字符串转换成整型会发生碰撞。同一个位置,可能映射多个字符串。

     因为字符串是无限的,而整型是有限的。不同字符串转整型,可能算出来的整型就是一样的。(1.用了不好的算式。不同字符串算出一样的数 2.超出整型范围截断后导致碰撞)

     2. 无法确定字符串范围。

     直接定址法的缺陷就是如果数据分散会导致空间浪费。除非有办法确定字符串范围,不然每次都开-1个位也太浪费了。

2. 布隆过滤器

布隆过滤器适用于整型以外的数据类型

对位图进行改进,通过映射多个位置的方式,降低误判率。


比如:一个字符串,用两个HashFunc转出两个整型,映射位图的两个位置。当两个位置都为1的时候,有可能这个字符串存在,也有可能还是和别的字符串冲突,但是映射多个位置能降低一些误判率。

2.1 应用场景

1.不要求百分百准确

布隆过滤器判断一个数存在是有可能会误判的,而判断一个数据不存在一定是准确的。

判断昵称是否重复:把所有已存在昵称放到布隆过滤器里面,布隆过滤器里能快速判断当前输入昵称不在。

客户端按ID查找某用户数据,数据存在服务器上的数据库。可以访问数据库之前,先访问布隆过滤器,如果布隆过滤器判断该ID不在,不需要再去数据库找,把不存在的ID过滤掉。如果布隆过滤器判断该ID存在(可能误判),再去数据库里找。当误判率是5%时,100个不存在的数据,5个判断成在,另外95个不存在直接过滤掉,提高了这95个数据的查找效率。

2.2 模拟

布隆过滤器采用的定址方法是除留余数法。开固定大小的空间。(空间开大开小都能映射,只是效率问题。空间大,碰撞概率低。空间小碰撞概率高。)

如何选择合适的函数个数k和过滤器长度m

k=m/n*ln2  ->【m=k/0.7*n】

#include<vector>
namespace my
{
	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;
		}
	};

	//N是数据个数
	//X是平均存一个数据所需开辟的位数。合理的、误报率低的X=k/0.7
	template <size_t N,
		size_t X=6,
		class K=string,
		class HashFunc1= BKDRHash,
		class HashFunc2= DJBHash,
		class HashFunc3= APHash>
	class BloomFilter
	{
	public:
		void set(const K& key)
		{
			size_t hashi1 = HashFunc1()(key) % (X * N);
			size_t hashi2 = HashFunc2()(key) % (X * N);
			size_t hashi3 = HashFunc3()(key) % (X * N);

			_bs.set(hashi1);
			_bs.set(hashi2);
			_bs.set(hashi3);
		}

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

			return true;//可能误判
		}
	private:
		std::bitset<N * X> _bs;
	};

	void test_bloomerfilter1()
	{
		string str[] = { "猪八戒", "孙悟空", "沙悟净", "唐三藏", "白龙马1","1白龙马","白1龙马","白11龙马","1白龙马1" };
		BloomFilter<10,5> bf;
		for (auto& str : str)
		{
			bf.set(str);
		}

		for (auto& s : str)
		{
			cout << bf.test(s) << endl;
		}
		cout << endl;

		srand(time(0));
		for (const auto& s : str)//测试误判,出现1就说明误判
		{
			cout << bf.test(s + to_string(rand())) << endl;
		}
	}
}

  • 不能reset,一个位置可能被多给数据映射,ret直接置成0,会影响其他数据。

强行支持reset的方法,计数。修改位图,需要多个位来标志一个数。空间消耗成倍增加。

2.3 运用

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

query一般是查询指令(如网络请求)或sql语句。假设平均每个query是50字节,100亿个就是500G。

  • 近似算法

近似算法对交集准确度没那么高,可以允许有不是交集的在放在交集里。使用布隆过滤器把A文件存入。遍历另一个文件,拿到B的query,test(query)。test返回true的就是交集。把交集放到一个容器里,最后去重。

因为布隆过滤器判断存在,是有可能误判的,即把不存在的数据判断成存在。布隆过滤器算出的交集一定包含所有真正的交集,不会漏。

  • 精确算法

和哈希切分那道的处理方法相似。

如果平均切分,一个query本来是交集,A文件的可能在A1号小文件,B文件的可能在B3号小文件,没办法找交集。

把两个文件哈希切用相同哈希函数分成n个小文件(编号i=HashFunc()(query)%1000)。

A文件中重复的query都在同一个小文件里,B文件中重复的也一定在同一个小文件。两文件交集一定在文件号数相同的Ai和Bi小文件里。

精确算法就把Ai和Bi小文件分别放进set1和set2,可以去重,同时在set1和set2的就是交集。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值