【数据结构】位图与布隆过滤器

目录

前言

位图的概念

经典面试题目

位图的模拟实现

set()

reset()

test()

位图整体代码

位图的应用

位图的优缺点

布隆过滤器

布隆过滤器的概念

哈希函数的个数与布隆过滤器长度的关系

布隆过滤器的模拟实现

插入

查找

删除

布隆过滤器整体代码


前言

哈希本质是一种映射(绝对映射/相对映射)的思想,哈希思想的应用之一便是位图,位图需要对比特位进行操作,因此需要回顾整型数据在内存中的存储与移位运算;

**思考:**如何将整型数据的第k个比特位设置为1或者0并且不影响其余的比特位 ?

无论当前机器是大端存储或者小端存储,由于数字1的最低的比特位为1,左移始终向高位移动,因此数字1先首先左移k个比特位,其次和原序列做或运算便设置第k个比特位为1;

同理,首先将数字1左移k个比特位,然后按位取反此时数字1所对应的二进制序列只有第k个比特位为0,其余的比特位皆为1,最后和原序列做与运算便可设置第k个比特位为0;

位图的概念

位图是一种基于位操作的数据结构,用于表示一组元素的集合信息;它通常是一个仅包含0和1 的数组,其中每个元素对应集合中的一个元素;位图中的每个位代表一个元素是否存在 于集合中当元素存在时,对应位的值为1;不存在时,对应位的值为0

经典面试题目

40亿个无符号整数,每个大小4个字节,则一共占用160亿字节,而1GB大约为10亿字节,则总共需要大约16GB,多数电脑的内存根本无法存放这些数据,若将数据存放于磁盘,进行外排序与二分查找,若在磁盘上进行,磁盘加载数据的速度缓慢,会导致效率降低,因此便采用位图解决;

内存中表示一个值是否存在的最小单位为比特位,由于数据范围为0 ~ 2^(32)-1,因此开辟2^(32)个比特位的空间,数据的值与存储位置构成绝对映射来标识该值是否存在;

位图的模拟实现

位图的底层结构为数组,采用vector充当底层容器,数组空间的开辟最小以字节为单位,因此既可以开辟整型数组也可以开辟字符型数组;本文采用开辟整型数组;

cpp 复制代码
//非类型模版参数N指定开辟多少比特位的空间
template<size_t N>
class BitSet
{
public:
    //构造函数中需要开辟空间,否则vector大小为0
	BitSet()
	{
		_bits.resize(N / 32 + 1, 0);
	}

private:
	vector<int> _bits;//开辟整型数组空间
};
  • 非类型模版参数N指定比特位的个数,而构造函数开辟的整型变量的个数,所以需要N/32;
  • 由于N/32的结果不是整数时会取整而抛弃小数部分,所以需要N/32+1,增加1个整型确保比特位足够映射集合中的数值;

set()

set设置x为存在,即将x映射的比特位设置为1;

cpp 复制代码
void set(size_t x)
{
	//计算x在第i个整型
	size_t i = x / 32;
	//计算x在第j个比特位
	size_t j = x % 32;

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

reset()

reset设置x为不存在,即将x映射的比特位设置为0;
清空位图中指定位的方法如下:

  1. 计算出该位位于第 i 个整型的第 j 个比特位;
  2. 将1左移 j 位然后按位取反最后和第 i 个整数进行与运算;
cpp 复制代码
//将x映射的比特位设置为0
void reset(size_t x)
{
	//计算x在第i个整型
	size_t i = x / 32;
	//计算x在第j个比特位
	size_t j = x % 32;

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

test()

检测某个比特位所标识的数值是否存在,存在则为真,否则为假;
获取位图中指定位的状态的方法如下:

  1. 计算出该位位于第 i 个整数的第 j 个比特位;
  2. 将1左移 j 位后与第 i 个整数进行与运算;
  3. 若结果非0,则该位所标识的数值存在,否则此比特位所标识的数值不存在;

位图整体代码

cpp 复制代码
template<size_t N>
class BitSet
{
public:
	BitSet()
	{
		_bits.resize(N / 32 + 1, 0);
	}
	//将x映射的比特位设置为1
	void set(size_t x)
	{
		//计算x在第i个整型
		size_t i = x / 32;
		//计算x在第j个比特位
		size_t j = x % 32;

		_bits[i] = _bits[i] | (1 << j);
	}
	//将x映射的比特位设置为0
	void reset(size_t x)
	{
		size_t i = x / 32;
		size_t j = x % 32;

		_bits[i] = _bits[i] & ~(1 << j);
	}
	//检测数值x是否存在
	bool test(size_t x)
	{
		size_t i = x / 32;
		size_t j = x % 32;

		return _bits[i] & (1 << j);
	}
private:
	vector<int> _bits;
};

位图的应用

位图的一个比特位只有两种状态标识数据是否存在,而统计次数数据可能出现0次、出现1次,出现1次以上具有三种状态,因此需要两个比特位来标识这三种状态;

cpp 复制代码
template<size_t N>
class two_bit_set
{
public:
	void set(size_t x)
	{
		// 原先为00,数据x出现后变为01
		if (_bs1.test(x) == false
			&& _bs2.test(x) == false)
		{
			_bs2.set(x);
		}
		else if (_bs1.test(x) == false
			&& _bs2.test(x) == true)
		{
			//原先为01,数据x出现后变为10
			_bs1.set(x);
			_bs2.reset(x);
		}
		//一次及以上不做处理
	}

	//数值x出现0次返回0,出现1次返回1,出现1次及以上返回2
	int test(size_t x)
	{
		if (_bs1.test(x) == false
			&& _bs2.test(x) == false)
		{
			return 0;
		}
		else if (_bs1.test(x) == false
			&& _bs2.test(x) == true)
		{
			return 1;
		}
		else
		{
			return 2; // 2次及以上
		}
	}
private:
	BitSet<N> _bs1;
	BitSet<N> _bs2;
};

位图的优缺点

优点:节省空间,效率高

缺点:一般要求数据范围相对集中,否则会导致空间消耗很大,位图只能处理整型数据,若内容编号为字符串,无法处理;

布隆过滤器

对于位图而言,只能处理整型数据,因为数据的数值采用 【直接定址法】计算哈希值几乎不会产生哈希冲突的问题,虽然字符串可以通过不同的哈希函数将字符串转换为整型,但是字符串的组合形式复杂多样,无论通过哪种哈希函数都不可避免地会出现大量哈希冲突;

此处的哈希冲突指不同的字符串被转换为相同的整型,此时便可能产生误判,即某个字符串明明不在数据集合中,却被系统判定为存在,于是诞生了布隆过滤器;

  • 位图中存在:不一定真正存在

如上图中"百度"和"百渡"转换为整型数值相同,那么映射的位置也相同,所以位图中第1234个比特位是1,就可以说"百度"和"百渡"都存在,但实际上是"百度"存在,而"百渡"不存在,于是产生了误判;

  • 位图不存在:必然不存在

若字符串"字节"转换为整型后与之对应的位图上的比特位为0,则说明"字节"不存在;

布隆过滤器的概念

布隆过滤器:当一个数据映射到位图中时**,** 采用多个哈希函数将其映射到多个比特位,当判断一个数据是否在位图当中时,需要分别根据这些哈希函数计算出对应的比特位,如果根据不同的哈希函数计算的比特位都设置为1则判定为该数据存在,否则判定为该数据不存在;

布隆过滤器使用多个哈希函数进行映射,目的在于【降低哈希冲突的概率】,一个哈希函数产生冲突的概率相对较高,但多个哈希函数同时产生冲突的概率会下降;
布隆过滤器极大的降低了哈希冲突的概率,但是仍然可能会产生误判:

  • 当布隆过滤器判断一个数据存在可能是不准确的,因为这个数据对应的比特位可能被其他一个数据或多个数据占用;
  • 当布隆过滤器判断一个数据不存在是准确的,因为该数据存在那么该数据对应的比特位都应该已经被设置为1;

哈希函数的个数与布隆过滤器长度的关系

问题是到底该创建多少个比特位的位图(布隆过滤器长度),又应该使用多少个哈希函数来映射一个字符串呢?

选取k=3,即设计3个哈希函数,则m约等于4.5倍n

布隆过滤器的模拟实现

cpp 复制代码
template<size_t N,
class K=string, //数据默认为字符串
class Hash1 = BKDRHash,//三种字符串哈希算法(将字符串转换为整型)
class Hash2 = APHash,
class Hash3 = DJBHash>
class bloomfilter
{
public:

private:
	static const size_t M = 5 * N;//M布隆过滤器长度=5*插入元素的个数
	//STL库中位图实现为静态数组(即int arr[]),存储在对象中,数据量大时可能会导致栈溢出,所以使用new开辟堆空间避免栈溢出
	std::bitset<M>* _bs = new std::bitset<M>;
};

选择三种字符串哈希算法,原文链接:https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html

cpp 复制代码
//BKDR版本
struct BKDRHash
{
	size_t operator()(const string& s)
	{
		size_t value = 0;
		for (auto ch : s)
		{
			value = value * 131 + ch;
		}
		return value;
	}
};
//AP版本
struct APHash
{
	size_t operator()(const string& s)
	{
		size_t value = 0;
		for (size_t i = 0; i < s.size(); i++)
		{
			if ((i & 1) == 0)
			{
				value ^= ((value << 7) ^ s[i] ^ (value >> 3));
			}
			else
			{
				value ^= (~((value << 11) ^ s[i] ^ (value >> 5)));
			}
		}
		return value;
	}
};
//DJB版本
struct DJBHash
{
	size_t operator()(const string& s)
	{
		if (s.empty())
			return 0;
		size_t value = 5381;
		for (auto ch : s)
		{
			value += (value << 5) + ch;
		}
		return value;
	}
};

插入

当元素插入到布隆过滤器时,布隆过滤器需要提供一个set接口,插入元素时,需要通过三个哈希函数分别计算出该元素对应的三个比特位,然后将位图中的映射的三个比特位设置为1即可;

cpp 复制代码
void set(const K& key)
{
	size_t hash1 = Hash1()(key) % M;
	size_t hash2 = Hash2()(key) % M;
	size_t hash3 = Hash3()(key) % M;

	_bs->set(hash1);
	_bs->set(hash2);
	_bs->set(hash3);
}

查找

布隆过滤器需要提供一个test接口,用于检测某个元素是否在布隆过滤器当中;

检测时,需要通过三个哈希函数分别计算出该元素对应的三个比特位,然后判断位图中映射的三个比特位是否被设置为1;

只要映射的三个比特位当中有一个比特位没有被设置为1则说明该元素一定不存在;

若映射的三个比特位全部被设置为1,则返回true表示该元素存在;

注意:判断存在的情况可能存在误判;

cpp 复制代码
bool test(const K& key)
{
	//依次判断key对应的三个位是否被设置
	size_t hash1 = Hash1()(key) % M;
	if (_bs->test(hash1) == false)
		return false;//key一定不存在

	size_t hash2 = Hash2()(key) % M;
	if (_bs->test(hash2) == false)
		return false;//key一定不存在

	size_t hash3 = Hash3()(key) % M;
	if (_bs->test(hash3) == false)
		return false;//key一定不存在

	return true; // 存在误判(有可能3个位都是跟别人冲突的,所以误判)
}

删除

布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素;

"百度"和"字节"映射的比特位都有第4个比特位,删除上图中"字节"元素,如果直接将该元素所对应的二进制比特位置0,则"百度"元素也被删除,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠;
一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作;
总结:布隆过滤器最好不要支持删除操作

布隆过滤器整体代码

cpp 复制代码
struct BKDRHash
{
	size_t operator()(const string& s)
	{
		size_t value = 0;
		for (auto ch : s)
		{
			value = value * 131 + ch;
		}
		return value;
	}
};
struct APHash
{
	size_t operator()(const string& s)
	{
		size_t value = 0;
		for (size_t i = 0; i < s.size(); i++)
		{
			if ((i & 1) == 0)
			{
				value ^= ((value << 7) ^ s[i] ^ (value >> 3));
			}
			else
			{
				value ^= (~((value << 11) ^ s[i] ^ (value >> 5)));
			}
		}
		return value;
	}
};
struct DJBHash
{
	size_t operator()(const string& s)
	{
		if (s.empty())
			return 0;
		size_t value = 5381;
		for (auto ch : s)
		{
			value += (value << 5) + ch;
		}
		return value;
	}
};
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 hash1 = Hash1()(key) % M;
		size_t hash2 = Hash2()(key) % M;
		size_t hash3 = Hash3()(key) % M;

		_bs->set(hash1);
		_bs->set(hash2);
		_bs->set(hash3);
	}
	bool test(const K& key)
	{
		size_t hash1 = Hash1()(key) % M;
		if (_bs->test(hash1) == false)
			return false;

		size_t hash2 = Hash2()(key) % M;
		if (_bs->test(hash2) == false)
			return false;

		size_t hash3 = Hash3()(key) % M;
		if (_bs->test(hash3) == false)
			return false;

		return true;
	}
private:
	static const size_t M = 5 * N;
	std::bitset<M>* _bs = new std::bitset<M>;
};

欢迎大家批评指正,博主会持续输出优质内容,谢谢各位观众老爷观看,码字画图不易,希望大家给个一键三连支持~ 你的支持是我创作的不竭动力~

相关推荐
wheeldown6 小时前
【数据结构】选择排序
数据结构·算法·排序算法
躺不平的理查德10 小时前
数据结构-链表【chapter1】【c语言版】
c语言·开发语言·数据结构·链表·visual studio
阿洵Rain10 小时前
【C++】哈希
数据结构·c++·算法·list·哈希算法
Leo.yuan10 小时前
39页PDF | 华为数据架构建设交流材料(限免下载)
数据结构·华为
半夜不咋不困10 小时前
单链表OJ题(3):合并两个有序链表、链表分割、链表的回文结构
数据结构·链表
忘梓.11 小时前
排序的秘密(1)——排序简介以及插入排序
数据结构·c++·算法·排序算法
y_m_h13 小时前
leetcode912.排序数组的题解
数据结构·算法
1 9 J14 小时前
数据结构 C/C++(实验三:队列)
c语言·数据结构·c++·算法
921正在学习编程14 小时前
数据结构之二叉树前序,中序,后序习题分析(递归图)
c语言·数据结构·算法·二叉树
毕竟秋山澪14 小时前
岛屿数量 广搜版BFS C#
数据结构·算法·宽度优先