C++——位图、布隆过滤器和哈希切分

文章目录

  • 一、位图
    • 1.1 概念
    • 1.2 模拟实现
    • 1.3 位图的应用及题型
  • 二、布隆过滤器
    • 1.1 概念
    • 1.2 模拟实现
    • 1.3 应用及题型


一、位图

1.1 概念

所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。
在STL容器中有实现。
C++——位图、布隆过滤器和哈希切分_第1张图片
C++——位图、布隆过滤器和哈希切分_第2张图片

1.2 模拟实现

位图的实现很简单,主要成员函数有置1(即插入),置0(删除),检测某个数是否存在

#pragma once
#include 
#include 

namespace ns_bit_set
{
	template<size_t N> // 非类型模板参数,表示位图的大小,能够表示数据的个数
	class BitSet
	{
	public:
		BitSet()
		{
			// 创建一个大小为N位的位图,但是要加1,因为这里的整除是向下取整
			_bits.resize(N / 8 + 1, 0);
		}
		// 将表示x的位置1
		void set(size_t x)
		{
			// 找到在第几个char里面,用i表示
			// 找到在一个char里面的第几位,用j表示
			size_t i = x / 8;
			size_t j = x % 8;

			// 将该位置1

			_bits[i] |= (1 << j);
		}
		// 删除某一位,即将对应的bit位置0
		void reset(size_t x)
		{
			size_t i = x / 8;
			size_t j = x % 8;

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

		// 检测某一个数是否存在
		bool test(size_t x)
		{
			size_t i = x / 8;
			size_t j = x % 8;

			// 大于0为真, 等于0为假
			return _bits[i] & (1 << j);
		}

	private:
		vector<char> _bits;
	};
}

1.3 位图的应用及题型

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

优点: 速度快,并且节省空间
缺点: 只可以映射整形(如果有负数则开两个图,取反,映射到另外一个图)

相关题型:

  1. 给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。
  • 遍历,时间复杂度O(N),但是每查找一次都是O(N)
  • 排序(O(NlogN)),利用二分查找: logN
  • 位图解决

40亿个整数大约需要40G的空间,一般计算机没有这么大的内存。
这个题最好是用位图解决,开一个32位无符号整数的最大范围的位图,然后将40亿个数存入位图即可。每次查询都是O(1)的时间复杂度。

  1. 给定100亿个整数,设计算法找到只出现一次的整数?
    判断一个值在不在,只需要两种状态,所以只使用一个位就可以。但是这里要找出值出现一次的数,其中有出现0次,出现1次, 出现2次及以上,此时有三种状态,说每个值使用两个位来表示就可以,出现0次用00表示,出现一次用01表示,出现两次及以上用10表示。然后遍历找出所有值为01的整数。

所以需要两个位图,最多可以表示四种状态
使用 00表示没有出现,01表示出现一次, 10表示出现两次及以上。

template<size_t N>
class TwoBitSet
{
public:
	void Set(size_t x)
	{
		if (!_bs1.test(x) && !_bs2.test(x)) // 00 -> 01
		{
			_bs2.set(x);
		}
		else if (!_bs1.test(x) && _bs2.test(x)) // 01 -> 10
		{
			_bs1.set(x);
			_bs2.reset(x);
		}
		// 10 表示已经出现2次或以上,不用处理
	}

	void PrintOnceNum()
	{
		for (size_t i = 0; i < N; ++i)
		{
			if (!_bs1.test(i) && _bs2.test(i)) // 01
			{
				cout << i << endl;
			}
		}
	}
private:
	bit::bitset<N> _bs1;
	bit::bitset<N> _bs2;
};
  1. 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?

方法一:将其中一个文件1的整数映射到一个位图中,读取另外一个文件2中的整数,判断在不在位图中,在就是交集。此时消耗的内存是整数的范围232,也就是512M。但有缺陷,遇到重复的值也要查找。
方法二:将文件1的整数映射到位图1中,将文件2的整数映射到位图2中,然后将两个位图中的数按位与,按位与之后为1的位就是交集,消耗内存1G。

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

和第二题一样,这次有四种状态:00 表示没有出现, 01表示出现一次,10表示出现两次,11表示出现三次及以上。所以同样可以用两个位图实现。类似的如果有6种状态就需要3个位图表示。

二、布隆过滤器

1.1 概念

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

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

将字符串映射为整数,但是字符串可以说是无穷的,那么就一定存在映射冲突。
布隆过滤器对某个东西的判断情况

  • 不存在。 是准确的,因为插入时使用的哈希函数和查询时的哈希函数是一样的。
  • 存在。 存在误判,因为存在冲突,也许是其他字符串映射。

降低误判的方式

  • 使用多种哈希算法,映射到不同的位置,只有当判断都存在的时候才是存在。 当然也不能完全解决误判

C++——位图、布隆过滤器和哈希切分_第3张图片
但是由于映射多个空间后,占用的位图空就比原来多了,所以位图的空间也要增加的。这也是减少冲突的有效方式。

所以位图空间的大小 m,映射函数的个数 k,插入元素的个数 n,怎么选择误判率p更低呢?

当然在条件允许的情况下,m 和 k越大越好,但是 m 的增大对误判率的减少是更有效的。
这里有个公式,可以选择合适的 k 和 m
C++——位图、布隆过滤器和哈希切分_第4张图片

布隆过滤器的优缺点
优点:

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

缺点:

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

1.2 模拟实现

#pragma once
#include 
#include 

// 三种字符串哈希方式
struct BKDRHash
{
	size_t operator()(const string& s)
	{
		// BKDR
		size_t value = 0;
		for (auto ch : s)
		{
			value *= 31;
			value += ch;
		}
		return value;
	}
};

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

// N 是要存储的个数, X是真正位图给的空间
template<size_t N, size_t X,
	class HashFunc1 = BKDRHash,
	class HashFunc2 = APHash,
	class HashFunc3 = DJBHash>
class BloomFilter
{
	void Set(const K& key)
	{
		size_t len = X * N;
		size_t index1 = HashFunc1()(key) % len;
		size_t index2 = HashFunc2()(key) % len;
		size_t index3 = HashFunc3()(key) % len;

		_bs.set(index1);
		_bs.set(index2);
		_bs.set(index3);
	}

	bool Test(const K& key)
	{
		size_t len = X * N;
		size_t index1 = HashFunc1()(key) % len;
		if (_bs.test(index1) == false)
			return false;

		size_t index2 = HashFunc2()(key) % len;
		if (_bs.test(index2) == false)
			return false;

		size_t index3 = HashFunc3()(key) % len;

		if (_bs.test(index3) == false)
			return false;

		return true;  // 存在误判的
	}

	// 不支持删除,删除可能会影响其他值。
	//void Reset(const K& key);
private:
	bitset<X* N> _bs;
};

1.3 应用及题型

布隆过滤器通常应用在允许误判的场景之中

  • 解决Redis缓存穿透问题
  • 邮件过滤,使用布隆过滤器来做邮件黑名单过滤
  • 对爬虫网址进行过滤,爬过的不再爬
  • 解决新闻推荐过的不再推荐(类似抖音刷过的往下滑动不再刷到)
  • HBase\RocksDB\LevelDB等数据库内置布隆过滤器,用于判断数据是否存在,可以减少数据库的IO请求

黑名单校验
发现存在黑名单中的,就执行特定操作。比如:识别垃圾邮件,只要是邮箱在黑名单中的邮件,就识别为垃圾邮件。假设黑名单的数量是数以亿计的,存放起来就是非常耗费存储空间的,布隆过滤器则是一个较好的解决方案。把所有黑名单都放在布隆过滤器中,再收到邮件时,判断邮件地址是否在布隆过滤器中即可。
身份验证:
比如注册账号时昵称或者用户名保证是不同的,当用户输入一个名称后,通过查找布隆过滤器,如果不存在就一定不存在,如果通过了布隆过滤器的判断,再去数据库中对比一次,这样通过一层布隆过滤器可以提高这个查找系统的效率

  1. 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和
    近似算法

近似算法:
将文件1中的query映射到一个布隆过滤器中,读取文件2中的query,判断在不在布隆过滤器中,在就是交集。
但是这是不准确的,由于存在误判,会把一些不是交集的查询也统计出来。

精确算法:
使用哈希切分,即对读入的查询使用哈希函数将查询分组,分组的大小由数据量决定,A和B对应的组里面若存在交集,则是A和B的交集。这是因为它们使用同样的哈希算法。这样求出每份小文件的交集之和就行。
但是如果是大量的交集,或者很多数据映射到同一个小文件导致依然无法读到内存,就需要换个哈希函数继续切分。

C++——位图、布隆过滤器和哈希切分_第5张图片

  1. 给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址? 与上题条件相同,如何找到top K的IP?
    先创建1000个小文件A0-A999,读取IP计算出i=hashstr(IP)%1000,i是多少,IP就进入对应编号的Ai小文件。这样相同的IP一定进入了同一个小文件。然后使用mapcountMap读取Ai中的IP统计出次数,一个文件读完,清空map,然后再读另一个文件,使用优先级队列priority_queue>存放每个文件最多的IP和对应次数,获得top K的IP。

你可能感兴趣的:(C++,c++,哈希算法,开发语言)