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

目录

前言

位图的概念

经典面试题目

位图的模拟实现

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充当底层容器,数组空间的开辟最小以字节为单位,因此既可以开辟整型数组也可以开辟字符型数组;本文采用开辟整型数组;

//非类型模版参数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;

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 个整数进行与运算;

//将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,则该位所标识的数值存在,否则此比特位所标识的数值不存在;

位图整体代码

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次以上具有三种状态,因此需要两个比特位来标识这三种状态;

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

布隆过滤器的模拟实现

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

//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即可;

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表示该元素存在;

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

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个计数器减一,通过多占用几倍存储空间的代价来增加删除操作;

总结:布隆过滤器最好不要支持删除操作

布隆过滤器整体代码

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>;
};

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

  • 39
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
位图(Bitmap)和布隆过滤器(Bloom Filter)都是常用的数据结构,用于处理大规模数据集合,但它们有着不同的应用场景和用途。 位图是一种压缩数据结构,用于快速地判断某个元素是否在集合中。位图的实现方式是将每个元素映射到一个二进制位上,如果该元素存在于集合中,则将对应的二进制位标记为1,否则标记为0。这样,当需要查询某个元素是否在集合中时,只需要查找对应的二进制位即可。由于位图的实现方式非常简单,因此可以快速地进行插入和查询操作,而且占用的空间也非常小,适合处理大规模数据集合。 布隆过滤器也是一种快速判断元素是否存在于集合中的数据结构,但其实现方式与位图略有不同。布隆过滤器使用一组哈希函数将元素映射到多个二进制位上,并将对应的二进制位标记为1。当查询某个元素是否在集合中时,将该元素进行哈希映射,并查找对应的二进制位,如果所有的二进制位都被标记为1,则说明该元素可能存在于集合中,否则可以确定该元素不存在于集合中。布隆过滤器的优点是可以快速地判断一个元素不存在于集合中,而且占用的空间也比较小,但存在误判率的问题。 因此,位图布隆过滤器虽然都可以用来处理大规模数据集合,但它们的实现方式和应用场景有所不同。位图适用于需要快速地判断某个元素是否在集合中的场景,而布隆过滤器适用于需要快速地判断一个元素不存在于集合中的场景。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小呆瓜历险记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值