【C++】位图和布隆过滤器

文章目录

  • 位图
    • 概念
    • 难点
    • 代码
  • 布隆过滤器
    • 概念
    • 插入
    • 查找
    • 删除
    • 优缺点
    • 代码

位图

概念

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

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

  1. 遍历,时间复杂度O(N)
  2. 排序(O(NlogN)),利用二分查找: logN
  3. 位图解决
    数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在。比如下图:

我们用 char 来实现位图,一个char类型的数据占8个字节,下标是 0 到 7 ,如下图 3 个 char 类型的数据,就可以表示 0 到 23 这些整数的状态(是否存在)。
【C++】位图和布隆过滤器_第1张图片

此外,在每一个 char 类型的数据内部,我们可以选择右低左高 或者 左低右高,如下图,选择右低左高的方式,这就类似于大端存储。
【C++】位图和布隆过滤器_第2张图片

难点

位图并不难理解,难点主要就是找到数据对应的比特位,然后将该比特位修改为 1 或者 0。

假设在位图 _b (用 char 实现)中寻找数据 x 对应的比特位分为两步:

  • 找到位于哪一个 char 类型的数据中。 i = x / 8;
  • 找到在该 char 类型的数据中,处于第几个位置。 j = x % 8;

这样子,我们就可以知道 x 在位图中的位于 _b[i] 的第 j 个位置。

  • 如果要标记 x 存在,也就是把对应的比特位修改为 1。_b[i] |= (1 << j);
  • 如果要标记 x 不存在,也就是把对应的比特位修改为 0。 _b[i] &= ~(1 << j);

1 << j ,代表着将 1 左移 j 位,然后 _b[i] 按位或 (1<左高右低,所以 1 左移 j 位。

标记 x 不存在,先将 1 左移 j 位,然后取反, 假设 j =3 , 那么 ~(1<<3) 的结果就是 11110111。用它和 _b[i] 按位与,由于第 j 位是 0,所以 _b[i] 必定是 0;其他七位是1,按位与之后 _b[i] 的其他七位保持原状。

代码

#pragma once
#include
#include

using namespace std;

template<size_t N>
class bitset
{
public:

	bitset()
	{
		_b.resize(N / 8 + 1, 0);
	}

	void set(size_t x)
	{
		size_t i = x / 8;
		size_t j = x % 8;

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

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

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


private:
	vector<char> _b;
};

布隆过滤器

概念

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

  1. 用哈希表存储用户记录,缺点:浪费空间。
  2. 用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理了。
  3. 将哈希与位图结合,即布隆过滤器。

布隆过滤器可以告诉我们 “某样东西一定不存在或者可能存在”,也就是说布隆过滤器说这个数不存在则一定不存,布隆过滤器说这个数存在可能不存在(误判,后面会讲到)。

其方法也不难理解,先将字符串进行哈希,映射到某个整数,然后用这个整数的状态(是否存在),标识这个字符串是否存在。( 整数的状态就可以用位图来表示。)
但是,为了保证正确性,一般会利用不同的哈希函数(正常是 3 个),分别对字符串进行哈希,得到多个不同的映射结果,用这多个结果共同标识字符串的存在与否(映射结果全部存在,字符串才存在,否则不存在)。

插入

如下,将字符串分别进行三次哈希映射,映射到三个整数,然后在位图中将这三个整数标记为存在(比特位修改为 1 ):

至于哈希函数,网络上有现成的,搜索即可。

【C++】位图和布隆过滤器_第3张图片

查找

布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特位一定为1。所以可以按照以下方式进行查找:分别计算每个哈希值对应的比特位置存储的是否为零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中。

布隆过滤器的查找 会存在误判的情况,如下图,插入 x,y,z ,并没有插入 w,可是,当我们判断 w 是否存在时,三个哈希函数映射的结果都是 1,从位图的角度来看, w 就是存在的,但是实际上是因为 x,y,z 的哈希函数映射结果和 w 的三个重合了!

【C++】位图和布隆过滤器_第4张图片

删除

布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。
比如:删除上图中"tencent"元素,如果直接将该元素所对应的二进制比特位置0,“baidu”元素也被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。
一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。

缺陷:

  1. 无法确认元素是否真正在布隆过滤器中
  2. 存在计数回绕

优缺点

布隆过滤器优点

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

布隆过滤器缺陷

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

代码


template<size_t N>
class bitset
{
public:

	bitset()
	{
		_b.resize(N / 8 + 1, 0);
	}

	void set(size_t x)
	{
		size_t i = x / 8;
		size_t j = x % 8;

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

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

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


private:
	vector<char> _b;
};


#pragma once
#include
#include

using namespace std;

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

		return hash;
	}
};

struct APHash
{
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (long i = 0; i < s.size(); i++)
		{
			size_t ch = s[i];
			if ((i & 1) == 0)
			{
				hash ^= ((hash << 7) ^ ch ^ (hash >> 3));
			}
			else
			{
				hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));
			}
		}
		return hash;
	}
};


struct DJBHash
{
	size_t operator()(const string& s)
	{
		size_t hash = 5381;
		for (auto ch : s)
		{
			hash += (hash << 5) + ch;
		}
		return hash;
	}
};


template<size_t N,
class K=string,
class Hash1=BKDRHash,
class Hash2=APHash,
class Hash3=DJBHash>
class BloomFilter
{
public:

	void set(const K& key)
	{
		size_t len = N * _x;
		size_t hash1 = Hash1()(key) % len;
		_bs.set(hash1);

		size_t hash2 = Hash2()(key) % len;
		_bs.set(hash2);

		size_t hash3 = Hash3()(key) % len;
		_bs.set(hash3);
	}

	bool test(const K& key)
	{
		size_t len = N * _x;

		// 只要有一个位置没有映射到,就是没有
		size_t hash1 = Hash1()(key) % len;
		if (!_bs.test(hash1))
		{
			return false;
		}

		size_t hash2 = Hash2()(key) % len;
		if (!_bs.test(hash2))
		{
			return false;
		}

		size_t hash3 = Hash3()(key) % len;
		if (!_bs.test(hash3))
		{
			return false;
		}

		return true;
	}


private:
	static const size_t _x = 5;
	bitset<N * _x> _bs;
};

你可能感兴趣的:(C++从入门到放弃,c++,开发语言)