海量数据处理(一) :位图与布隆过滤器的概念以及实现


位图

位图概念

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

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

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

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

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

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


位图的应用

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

位图的实现思路

为了方便实现,位图的底层可以使用一个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即将对应标识位置1

首先让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则说明不存在

test

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

	return _bits[index] & (1 << pos);
}

完整代码

#pragma once
#include<vector>

namespace lee
{
	class bitset
	{
	public:
		//每一个位标识一个数据,一个整型4个字节,可存储32个位, 所以需要/32(或者右移五位)。+1是为了取整
		bitset(size_t size = 32) 
			: _bits((size >> 5) + 1, 0)
			, _size(0)
		{}

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

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

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

			//对应位置数据置零
			_bits[index] &= ~(1 << pos);
			++_size;
		}
		
		//判断数据是否存在
		bool test(size_t x) const
		{
			size_t index = x >> 5;
			size_t pos = x % 32;

			return _bits[index] & (1 << pos);
		}

		size_t size() const
		{
			return _size;
		}

	private:
		std::vector<int> _bits;
		size_t _size;
	};
};

布隆过滤器

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

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

布隆过滤器概念

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

在这里插入图片描述


布隆过滤器的优缺点

优点

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

缺点

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

布隆过滤器的实现思路

这里底层使用的数据结构是前面实现的位图,所以对应操作可以直接到上面看。

哈希冲突的问题

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

例如下面的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;
	};
};

参考文章:

猜你喜欢

转载自blog.csdn.net/qq_35423154/article/details/107363058