位图和布隆过滤器

位图其实就是哈希的变形,他同样通过映射来处理数据,只不过位图本身并不存储数据,而是存储标记。通过一个比特位来标记这个数据是否存在,1代表存在,0代表不存在。

位图通常情况下用在数据量庞大,且数据不重复的情景下判断某个数据是否存在。

例如下面这道十分经典的题目

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

关于这道题目,解法其实有很多。
1.快速排序后二分搜索。(内存可能不够,要16G内存)
2.位图处理,(40亿无符号整数用位图标记只需要512M的内存)

位图的解法差不多是这道题的最优解,只需要将所有数据读入后将对应位置置1,然后再查找那个数据所储的位置是否为1即可。

位图的应用
快速查找某个数据是否在一个集合中
排序
求两个集合的交集、并集等
操作系统中磁盘块标记
位图的实现思路
为了方便实现,位图的底层可以使用一个vector。而开空间并不根据数据的个数来开,而是根据数据的范围来开(如果开的空间不够,可能有位置无法映射到)。并且一个整型具有32个字节,所以如果我们要存N个数据,就只需要开N / 32 + 1的空间即可(+1是为了防止数据小于32和向上取整)。

当要操作一个数据时,先将其除以32来判断它应该处于数组中哪一个整型中。再对其%32,来判断它位于这个整型中的哪一个位上,此时再进行对应的位运算即可。

set
set即将对应标识位置1
可以通过将1左移pos个位置,再让对应位置与这个数据相或即可实现。

//数据的对应标识位置1
void set(size_t x)
{
	//计算出在数组中哪一个整型中
	size_t index = x >> 5;
	//计算出在该整型的哪一个位上
	size_t pos = x % 32;

	//对应位置 置1
	_bits[index] |= (1 << pos);
	++_size;
}
reset

reset即将对应标识位置0

首先让1左移pos个位置,再对这个数据进行取反。然后让对应位置数据与这个数据相即可。

//数据的对应标识位置0
void reset(size_t x)
{
	size_t index = x >> 5;
	size_t pos = x % 32;

	//对应位置数据置零
	_bits[index] &= ~(1 << pos);
	++_size;
}

test即判断这个数据在不在,只需要让1左移pos个位置,再用对应位置进行与运算,如果为1则说明存在,0则说明不存在

bool test(size_t x) const
{
	size_t index = x >> 5;
	size_t pos = x % 32;

	return _bits[index] & (1 << pos);
}
完整代码

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

用哈希表存储用户记录,缺点:浪费空间
用位图存储用户记录,缺点:不能处理哈希冲突
将哈希与位图结合,即布隆过滤器


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


布隆过滤器的优缺点
优点
增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
哈希函数相互之间没有关系,方便硬件并行运算
布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
使用同一组散列函数的布隆过滤器可以进行交、并、差运算
缺点
有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白 名单,存储可能会误判的数据)
不能获取元素本身
一般情况下不能从布隆过滤器中删除元素
如果采用计数方式删除,可能会存在计数回绕问题
布隆过滤器的实现思路
这里底层使用的数据结构是前面实现的位图,所以对应操作可以直接到上面看。

哈希冲突的问题
之前在哈希那一章说过,当字符串使用哈希时,无可避免的会出现哈希冲突的问题,而位图又是一个不能解决哈希冲突的数据结构,所以这就导致了一个问题,对于一个数据不能只有一个位置来标记,需要用到多个位置。于是我们需要用到多个哈希函数,来将数据映射到多个位置上面,才能确保数据的准确性。

例如下面的baidu,分别通过三种哈希函数映射到了1,4,7。将这三个位置全部置1


这里我使用了三个字符串哈希函数,分别是BKDR,SDBM,RS。

struct _BKDRHash
{
	//BKDRHash
	size_t operator()(const std::string& key)
	{
		size_t hash = 0;

		for (size_t i = 0; i < key.size(); i++)
		{
			hash *= 131;
			hash += key[i];
		}
		return hash;
	}
};

struct _SDBMHash
{
	//SDBMHash
	size_t operator()(const std::string& key)
	{
		size_t hash = 0;

		for (size_t i = 0; i < key.size(); i++)
		{
			hash *= 65599;
			hash += key[i];
		}
		return hash;
	}
};

struct _RSHash
{
	//RSHash
	size_t operator()(const std::string& key)
	{
		size_t hash = 0;
		size_t magic = 63689;

		for (size_t i = 0; i < key.size(); i++)
		{
			hash *= magic;
			hash += key[i];

			magic *= 378551;
		}
		return hash;
	}
};

如何选择哈希函数个数和布隆过滤器长度
而如果一个数据要映射多个位置,如果布隆过滤器较小,则会导致数据马上全部映射满,此时无论进行什么操作,都会存在大量的误报率。也就是说,布隆过滤器的长度与误报率成反比,与空间利用率成反比。
并且哈希函数的个数也值得思考,哈希函数越多,映射的位置也就越多,此时准确性也就越高,但随之带来的问题就是效率的降低。也就是说,哈希函数的个数与效率成反比,准确率成正比

这张图则是各种长度以及哈希函数的效率对比图。

那么该如何选择哈希函数的个数以及布隆过滤器的长度呢?

k 为哈希函数个数,m 为布隆过滤器长度,n 为插入的元素个数,p 为误报率。

所以根据公式,我这里使用的哈希函数为3个,空间就应该开插入元素个数的五倍。

插入

数据分别映射到三个位置上,将三个位置全部置1

void set(const K& key)
{
	//为了减少错误率,用多个哈希函数将同一个数据映射到多个位置
	size_t pos1 = Hash1()(key) % _capacity;
	size_t pos2 = Hash2()(key) % _capacity;
	size_t pos3 = Hash3()(key) % _capacity;

	_bs.set(pos1);
	_bs.set(pos2);
	_bs.set(pos3);

	++_size;
}

 

查找
布隆过滤器的查找即分别查找映射位,一旦有任何一个为0,则说明数据不存在。如果全部为1,此时说明数据可能存在,因为可能存在将别人映射的位置误判进来,所以布隆过滤器的查找是不够准确的。所以可以这么说,布隆过滤器只提供模糊查询,如果需要精确查询,只能使用别的方法。

布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可能存在,因
为有些哈希函数存在一定的误判。
 

bool test(const K& key)
{
	size_t pos1 = Hash1()(key) % _capacity;
	size_t pos2 = Hash2()(key) % _capacity;
	size_t pos3 = Hash3()(key) % _capacity;

	if (!_bs.test(pos1) || !_bs.test(pos2) || !_bs.test(pos3))
	{
		return false;
	}

	return true;
}

删除
布隆过滤器是不支持删除操作的,因为一旦进行删除,很可能就会将别人映射的位置也置为0,导致出现错误。

但是如果非要删除的话,也不是不行。
可以将每一个比特位拓展为一个计数器,每当有数据插入时对应位置的计数器+1,数据删除是对应位的计数器-1。一个位肯定无法完成计数,需要用到多个位,此时就会导致存储空间的大量增加,使得效率下降,而本身选择布隆过滤器也是为了节省空间,这样就本末倒置了。

完整代码
 

#pragma once
#include"bitset.hpp"
#include<string>
namespace lee
{
	struct _BKDRHash
	{
		//BKDRHash
		size_t operator()(const std::string& key)
		{		
			size_t hash = 0;

			for (size_t i = 0; i < key.size(); i++)
			{
				hash *= 131;
				hash += key[i];
			}
			return hash;
		}
	};

	struct _SDBMHash
	{
		//SDBMHash
		size_t operator()(const std::string& key)
		{
			size_t hash = 0;

			for (size_t i = 0; i < key.size(); i++)
			{
				hash *= 65599;
				hash += key[i];
			}
			return hash;
		}
	};

	struct _RSHash
	{
		//RSHash
		size_t operator()(const std::string& key)
		{
			size_t hash = 0;
			size_t magic = 63689;

			for (size_t i = 0; i < key.size(); i++)
			{
				hash *= magic;
				hash += key[i];

				magic *= 378551;
			}
			return hash;
		}
	};

	template<class K = std::string, class Hash1 = _BKDRHash, class Hash2 = _SDBMHash, class Hash3 = _RSHash>
	class BloomFilter
	{
	public:
		BloomFilter(size_t num)
			: _bs(num)
			, _capacity(num)
			, _size(0)
		{}

		void set(const K& key)
		{
			//为了减少错误率,用多个哈希函数将同一个数据映射到多个位置
			size_t pos1 = Hash1()(key) % _capacity;
			size_t pos2 = Hash2()(key) % _capacity;
			size_t pos3 = Hash3()(key) % _capacity;

			_bs.set(pos1);
			_bs.set(pos2);
			_bs.set(pos3);

			++_size;
		}

		bool test(const K& key)
		{
			size_t pos1 = Hash1()(key) % _capacity;
			size_t pos2 = Hash2()(key) % _capacity;
			size_t pos3 = Hash3()(key) % _capacity;

			if (!_bs.test(pos1) || !_bs.test(pos2) || !_bs.test(pos3))
			{
				return false;
			}

			return true;
		}

		size_t size() const
		{
			return _size;
		}

	private:
		lee::bitset _bs;
		size_t _size;
		size_t _capacity;
	};
};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值