【C++】哈希的应用 -- 布隆过滤器

news2024/10/5 19:18:40

文章目录

  • 一、布隆过滤器的引入
  • 二、哈希函数个数的选择
  • 三、布隆过滤器的实现
  • 四、布隆过滤器的应用
  • 五、布隆过滤器总结

一、布隆过滤器的引入

我们在上一节中学习了 位图,知道了位图可以用来快速判断某个数据是否在一个集合中,但是位图有如下的缺点

  1. 位图只适用于数据范围集中的情况,当数据范围分散时,存在空间浪费;
  2. 位图只能针对整形,对于非整形数据它不能处理。

其中位图只能针对整形这一缺陷我们可以想办法解决,其中最常见的方法就是针对某一种特定类型定义一个 HashFunc 函数,将其转化为整形;比如当数据是 string 类型时,我们可以使用字符串哈希算法将字符串转化为整形,然后再将这个整形映射到位图中;

但是这种方法存在一种很大的缺陷 – 不同的字符串通过同一个 HashFunc 函数转换出来的值可能是一样的,也就是说,可能会发生误判 (哈希冲突),在这种情况下:

  • 位图中该字符串存在是不准确的,因为该比特位可能原本为0,但是和其他字符冲突,发生了误判,导致该比特位变为1;
  • 位图中字符串不存在是准确的,因为比特位为0说明该字符串以及可能与该字符串发生冲突的其他字符串都没有插入过,当然前提是不考虑删除的情况。

同时,由于通过字符串哈希函数转换出来的值的范围是不确定的,所以我们通常会对结果进行取模,以此来节省空间,但是取模又会增加哈希冲突的概率,因为不同的整形取模后得到结果可能是一样的。

那么我们如何降低误判率呢?此时布隆过滤器就登场了。

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

可以看到,布隆过滤器通过使用多个哈希函数的方法来降低误判率,即让同一个元素映射多个下标位置,在查询时只有当这些位置都为1时才表示该元素存在,而同一元素通过不同哈希函数映射出的不同下标同时被误判的概率肯定是比一个下标位置被误判的概率要低很多的。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WrhcapPx-1681382089012)(https://picgo-tian1.oss-cn-beijing.aliyuncs.com/img/202304120044727.png)]

特别注意:布隆过滤器只能降低误判率,而不能彻底消除误判。


二、哈希函数个数的选择

那么是不是映射的下标位置越多越好呢?当然不是,因为一个元素映射的下标位置越多,那么浪费的空间也就越多;所以有的大佬就针对如何选择哈希函数个数和布隆过滤器长度专门写了一篇博客,大家可以参考参考:详解布隆过滤器的原理,使用场景和注意事项 - 知乎 (zhihu.com)

其博客中给出了哈希长度、布隆过滤器长度、插入元素个数与误判率的关系图:image-20230412001109387

以及它们之间的关系式:image-20230412001758953

对上面的K取值进行带入可得:

  • k == 3 时,m ≈ 4.3 n;即一个元素要消耗四个左右的比特位;
  • k == 5 时,m ≈ 7.2 n;即一个元素要消耗七个左右的比特位;
  • k == 8 时,m ≈ 11.6 n;即一个元素要消耗12个左右的比特位;

结合关系图和关系表达式可以看出,哈希函数的个数取 3~5 个是比较合适的,取8的话空间消耗有点大,但是忍忍其实也能接受。


三、布隆过滤器的实现

布隆过滤器的实现其实很简单,位图直接使用库中的 bitset 即可,字符串哈希算法可以从下面这篇博客介绍的算法里面挑选几个得分比较高的:各种字符串Hash函数 - clq - 博客园 (cnblogs.com);

代码实现如下:

#pragma once

#include <bitset>
#include <string>
using std::bitset;
using std::string;

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

struct APHash {
	size_t operator()(const string& s) {
		size_t hash = 0;
		for (size_t 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;
	}
};

template<size_t N,	//数据范围
	size_t X = 5,    //每个元素最多消耗的比特位的位数
	class K = string,    //数据类型
	class HashFunc1 = BKDRHash,   //哈希函数
	class HashFunc2 = APHash,
	class HashFunc3 = DJBHash>
	class BloomFilter {
	public:
		void set(const K& key) {
			size_t hash1 = HashFunc1()(key) % (N * X);
			size_t hash2 = HashFunc2()(key) % (N * X);
			size_t hash3 = HashFunc3()(key) % (N * X);

			//将三个位置的比特位都置1才表示插入key
			_bs.set(hash1);
			_bs.set(hash2);
			_bs.set(hash3);
		}

		bool test(const K& key) {
			size_t hash1 = HashFunc1()(key) % (N * X);
			size_t hash2 = HashFunc2()(key) % (N * X);
			size_t hash3 = HashFunc3()(key) % (N * X);

			//key映射的三个下标位置都为真才表示key可能存在
			//也可能不存在,此时映射的位置全部冲突,从而发生误判
			if (_bs.test(hash1) && _bs.test(hash2) && _bs.test(hash3)) {
				return true;
			}

			return false;
		}
	private:
		bitset<N* X> _bs;
};

在我们上面的实现中,第一个模板参数N为数据的范围,第二个X为每一个数据最多占用多少个比特位,它与哈希函数的个数有关,由于我们实现的版本中默认使用的是三个哈希函数,所以X的缺省值为5,但我们也可以显示传递X的值来增加/减少哈希冲突的概率,最后三个模板参数分别为三个哈希函数,这里我们使用的字符串哈希算法分别为BKDRHash、APHash 和 DJBHash;对程序进行简单测试结果如下:

image-20230412011831223

image-20230412011900623

在上面的测试程序中,由于每次产生的数是随机的,所以测试结果有时会发生误判,有时不会发生误判。

现在我们加大测试用例,并分别构造相似字符串集和不相似字符串集来分别测试其误判率,测试代码如下:

void BloomFilter_test2()
{
	srand(time(0));
	const size_t N = 100000;
	BloomFilter<N> bf;

	std::vector<std::string> v1;
	std::string url = "https://coder-yzpq.blog.csdn.net/?type=blog";

	for (size_t i = 0; i < N; ++i)
	{
		v1.push_back(url + std::to_string(i));
	}

	for (auto& str : v1)
	{
		bf.set(str);
	}

	// v2跟v1是相似字符串集
	std::vector<std::string> v2;
	for (size_t i = 0; i < N; ++i)
	{
		std::string url = "https://coder-yzpq.blog.csdn.net/?type=blog";
		url += std::to_string(999999 + i);
		v2.push_back(url);
	}

	size_t n2 = 0;
	for (auto& str : v2)
	{
		if (bf.test(str))
		{
			++n2;
		}
	}
	cout << "相似字符串误判率:" << (double)n2 / (double)N << endl;

	// v3跟v1是不相似字符串集
	std::vector<std::string> v3;
	for (size_t i = 0; i < N; ++i)
	{
		string url = "csdn.com";
		url += std::to_string(i + rand());
		v3.push_back(url);
	}

	size_t n3 = 0;
	for (auto& str : v3)
	{
		if (bf.test(str))
		{
			++n3;
		}
	}
	cout << "不相似字符串误判率:" << (double)n3 / (double)N << endl;
}

n == 10 万, k == 3, X == 5 时,测试结果如下:image-20230412014241859

n == 10 万, k == 3, X == 8 时,测试结果如下:image-20230412014107700

n == 10 万, k == 3, X == 12 时,测试结果如下:image-20230412014151090

n == 100万, k == 3, X == 12 时,测试结果如下:image-20230412014550862

从这些测试结果中可以看出,布隆过滤器虽然存在误判的情况,但其误判率是可控的 – 我们可以根据具体的应用场景来测试调整哈希函数的个数以及布隆过滤器的长度,最终实现出最符合当前应用场景的布隆过滤器

布隆过滤器的删除布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素;但是我们也可以使用计数的方式强行让其支持删除操作,即使用多个位图来标记某一个元素出现的次数,其思路和 位图 中查找出现一次或两次的元素的思路一样,不过这里还存在一个问题 – 我们不知道元素最多的出现次数为几,所以无法确定要使用几个位图来标记一个元素;所以如果不是在某些特殊场景下布隆过滤器是不支持删除操作的。


四、布隆过滤器的应用

布隆过滤器适用于不需要完全准确,允许出现一定误判的场景,例如如下场景:

  • 用户注册时的昵称判重:某些网站在注册不允许出现重复昵称,而已注册的昵称都保存在服务器的数据库中,因为数据库存放在磁盘上,访问速度非常慢,所以如果用户每选择一个昵称都去数据区中查找是否已经存在的话其效率就会非常低,并且在实际中用户昵称已存在的概率是比较大的

    这时我们就可以在服务器前面加一个布隆过滤器进行过滤 – 将所有已注册的昵称都映射到布隆过滤器中,如果该昵称没被注册,则该昵称不在布隆中,而不在是一定准确的,此时允许用户使用该昵称;如果该昵称在布隆中,说明该昵称已被使用,则提示用户重新输入;尽管昵称在可能会发生误判,但这并不影响用户的使用,仅仅相当于发生误判的昵称不允许被任何人使用而已;我们也可以当在时再去数据库中查找存在该昵称是否真的存在,从而保存查询结果的完全准确,但在此场景下是没必要的。

    如上,我们通过添加一个布隆过滤器就能过滤掉大部分无用的查询请求,从而有效提高服务器的性能。(注:在实际的联网软件中此方法不可行,因为可能存在多个用户在不同的客户端同时注册相同昵称的场景,此方法只适用于单机的场景,但这里也仅仅是用其举例而已)

  • 查询个人数据:比如我们要在公司的客户资料数据库中以身份证号码为key值查找某一个客户的具体信息,由于直接访问数据库效率非常低,且大部分情况下该身份证号码所对应的客户都不在公司的数据库中;

    所以我们可以选择在公司服务器前面加一个布隆过滤器,其中映射了所有公司客户的身份证号码,当我们进行查询时先到布隆过滤器中进行查询,如果不在则直接返回不在,且返回结果一定是准确的;如果在那么结果不一定准确,我们还需要进一步到服务器的数据库中去查找该客户,如果查找成功就返回该客户的所有资料,如果发生误判即不在仍然返回不在即可;这样我们也能有效提高服务器的性能。

在实际开发中布隆过滤器的应用场景还有许多,比如网站黑名单的设计等;所以布隆过滤器在实际开发中是比较重要的,在面试时被考察的也比较多,大家需要理解它的原理,特别是布隆过滤器到底是在是正确的还是不在是准确的,大家必须要能够正确回答并且清晰阐释这个问题。


五、布隆过滤器总结

布隆过滤器的引出

  • 解决位图只能处理整形和数据范围集中的缺陷 – 哈希函数和取模,但这样会导致哈希冲突从而发生误判,为了降低误判率我们需要合理选择哈希函数的个数以及布隆过滤器的长度。

布隆过滤器的优点

  • 增加和查询元素的时间复杂度为 O(K),与数据量大小无关;(K为哈希函数的个数,一般都不会超过10)
  • 不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势;
  • 在允许一定误判率的场景中,具有很大的空间优势和时间优势;
  • 数据量很大时,布隆过滤器可以表示全集;
  • 使用同一组散列函数的布隆过滤器可以进行交、并、差运算,从而实现计数功能。

布隆过滤器的缺点

  • 有一定的误判率,即存在假阳性,不能准确判断元素是否在集合中,但误判率是可控的;(补救方法:建立一个白名单,其中存储可能会误判的数据)
  • 不能获取元素本身;
  • 一般情况下不能从布隆过滤器中删除元素;
  • 如果采用计数方式进行删除,会存在空间浪费,还可能会存在计数回绕问题。(计数回绕是指在计数的过程中,当计数器达到其最大值之后,继续累加将导致计数器值回到零)

最后,给出一道与布隆过滤器相关的面试题:给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法。

解析:这道题和上一节 位图 中求IP地址个数那道题一样,都是考察哈希切割 – 使用相同的哈希函数分别对这两个文件进行切割,切割结果为 A0 ~ Ai,B0 ~Bi,因为哈希函数相同,所以 Ai 和 Bi 中相同的 query 及发生冲突的 query 都在同一个小文件中,此时我们只需要分别求出 Ai 和 Bi 相同下标小文件中的交集即可,需要注意的是,如果小文件很大,说明某一个或某几个 query 有大量重复,此时换一个哈希函数再分别对 Ai 和 Bi 小文件递归子问题进行哈希切割即可;

对于精确算法来说,我们需要先将 Ai 号小文件中的元素全部存入 set/map 中,再依次取 Bi 号小文件中的数据到 set/map 中查询即可得到交集,注意结果需要去重;

对于近似算法来说,我们可以先将 Ai 号小文件中的元素全部映射到一个布隆过滤器中,然后再依次取 Bi 号小文件中的数据到布隆过滤器中查询即可得到交集,注意结果也需要去重。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/419196.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Qemu虚拟机读取物理机的物理网卡的流量信息方法

项目背景&#xff1a; 比如我有三个项目 A&#xff0c;B&#xff0c;C&#xff1b;其中A项目部署在物理机上&#xff0c;B&#xff0c;C项目部署在 虚拟机V1,V2中&#xff0c;三个项目接口需要相互调用。 需要解决的问题点&#xff1a; 1&#xff0c;因为A&#xff0c;B&#x…

2016-2017 ACM-ICPC Nordic Collegiate Programming Contest (NCPC 2016)题解

2016-2017 ACM-ICPC Nordic Collegiate Programming Contest (NCPC 2016) A - Artwork 题目描述&#xff1a; 给定N*M的网格&#xff0c;给出Q次询问&#xff0c;每次询问都给出一个小矩阵&#xff0c;保证每个矩阵要么长为1&#xff0c;要么宽为1&#xff0c;将网格中矩阵部…

最近给shopify跨境电商网站搞google搜索引擎的seo优化,整理了一些内容

接到一个网站&#xff0c;首先要做一些工作&#xff0c;然后按照这个步骤做好每一步&#xff0c;网站的搜索排名会有明显的效果。 对网站进行技术审核&#xff0c;以确保它符合搜索引擎的技术要求。研究关键词并确定目标关键词。优化网站内容&#xff0c;以便更好地针对目标关…

【LeetCode】剑指 Offer 55. 二叉树的深度 p271 -- Java Version

1. 题目介绍&#xff08;55. 二叉树的深度 &#xff09; 面试题55&#xff1a;二叉树的深度&#xff0c; 一共分为两小题&#xff1a; 题目一&#xff1a;二叉树的深度题目二&#xff1a;平衡二叉树 2. 题目1&#xff1a;二叉树的深度 题目链接&#xff1a;https://leetcode.c…

简单写一个Avue增删改查

今天练习了一下avue&#xff0c;真的好用&#xff0c;个人感觉相比于用element plus的组件还方便&#xff01; 简简单单的写了一个页面的增删改查&#xff0c;思路很简单。如果在写那种后台管理项目&#xff0c;基本上全是列表页&#xff0c;用这种方法写出来第一页&#xff0c…

收入下滑,亏损扩大的人力资源管理公司罗科仕申请纳斯达克IPO上市

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 猛兽财经获悉&#xff0c;来自北京的人力资源管理公司罗科仕近期已向美国证券交易委员会&#xff08;SEC&#xff09;提交招股书&#xff0c;申请在纳斯达克IPO上市&#xff0c;股票代码为(LGCL) 。罗科仕计划通过此次纳斯…

算法训练第五十八天 | 739. 每日温度、496.下一个更大元素 I

单调栈part01739. 每日温度题目描述思路496.下一个更大元素 I题目描述思路739. 每日温度 题目链接&#xff1a;739. 每日温度 参考&#xff1a;https://programmercarl.com/0739.%E6%AF%8F%E6%97%A5%E6%B8%A9%E5%BA%A6.html 题目描述 请根据每日 气温 列表&#xff0c;重新生…

Android组件化开发

Android组件开发 一、背景 一个app随着业务增加&#xff0c;代码放在同一个模块中会越来越臃肿&#xff0c;同时也导致多人开发的一个难度。组件化可以把业务单独分出来&#xff0c;形成一个单独模块&#xff0c;可单独运行、测试等&#xff0c;相互之间不会影响。另外一个优…

鼎捷T100制造之工艺工单实战(其他工艺补充)

文章目录 一、网状工艺二、平行工艺三、替代工艺四、返工工艺五、无顺序工艺一、网状工艺 网状工艺类似一张网状结构。可以包含平行和线性工艺等于一体。 网状工艺: 产品结构 aeci004:建立作业 aecm200: 工艺路线维护

kubeasz搭建k8s集群-部署单节点集群(AllinOne部署)

1说明 kubeasz 致力于提供快速部署高可用k8s集群的工具, 同时也努力成为k8s实践、使用的参考书&#xff1b;基于二进制方式部署和利用ansible-playbook实现自动化&#xff1b;既提供一键安装脚本, 也可以根据安装指南分步执行安装各个组件。 kubeasz 从每一个单独部件组装到完…

太赫兹高速通信系统前端关键技术

摘要&#xff1a;对构成太赫兹无线系统的2 种关键电路&#xff08;分谐波混频器和二倍频器&#xff09;进行了深入研究。在关键电路研究取得突破的基础上&#xff0c;开展了太赫兹无线通信技术研究&#xff0c;构建了220 GHz 无线通信实验验证系统。220 GHz 实验验证系统在室外…

elasticsearch 其他字段类型详解和范例

本章主要内容 elasticsearch 中别名字段的详解和范例elasticsearch 中二进制类型的详解和范例elasticsearch 中的嵌套类型的详解和范例elasticsearch 中的范围类型的详解和范例elasticsearch 中的排名类型的详解和范例elasticsearch 中的ip类型的详解和范例elasticsearch 中的…

【网络应用开发】实验3——Web组件重用与JavaBeans

目录 Web组件重用与JavaBeans预习报告 一、实验目的 二、实验原理 三、实验预习内容 1. 静态include指令何时执行&#xff1f;主页面和被包含的子页面是否转换为一个转换单元&#xff1f; 2.动作指令何时执行&#xff1f;主页面和被包含的子页面是否转换为一个转换单元&a…

【Python游戏】坦克大战、推箱子小游戏怎么玩?学会这些让你秒变高高手—那些童年的游戏还记得吗?(附Pygame合集源码)

前言 下一个青年节快到了&#xff0c;想小编我也是过不了几年节日了呢&#xff01;&#xff01; 社交媒体上流传着一张照片——按照国家规定“14岁到28岁今天都应该放半天假&#xff01;”不得不说&#xff0c; 这个跨度着实有点儿大&#xff0c;如果按整自然年来算年龄&…

Spark 连接 Hive

目录 导包 修改配置文件 修改hive-site.xml文件 启动hadoop 启动hive 启动spark 测试 查看表 导包 spark连接hive需要六个关键的jar包&#xff0c;以及将hive的配置文件hive-site.xml拷贝到spark的conf目录下。如果你hive配置没问题的话&#xff0c;这些jar都在hive的目…

Spring Security实战(四)—— 会话管理

目录 一、理解会话 二、防御会话固定攻击 三、会话过期 四、会话并发控制 五、集群会话的缺陷 六、集群会话的解决方案 七、整合Spring Session解决集群会话问题 只需在两个浏览器中用同一个账号登录就会发现&#xff0c;系统并没有任何会话并发限制。一个账号可以多处同…

cuSPARSE官方程序示例

cuSPARSE Library 简介 这个文件演示了cuSPARSE通用API的用法 官方程序&#xff1a;后续会出解析&#xff08;20230410&#xff09; cuSPARSE Generic APIs Documentation cuSPARSE Samples 向量 - 向量 操作矩阵 - 向量 操作矩阵 - 矩阵操作转换Legacy APIs优化稀疏迭代…

阿里本地生活再出发:口碑入高德,备战美团、抖音

配图来自Canva可画 近日&#xff0c;有传言称高德地图将和阿里本地生活旗下的到店业务口碑正式合并&#xff0c;未来阿里旗下所有的本地生活到店业务都将统一整合在高德地图的入口中。3月22日&#xff0c;高德地图正式确认了此事&#xff0c;并表示高德地图作为“出门好生活开…

网络软件-管理网络设备和组件

网络软件是一个术语&#xff0c;表示帮助网络管理员轻松执行特定操作的任何网络软件。术语“网络软件”可以包括广泛的软件解决方案&#xff0c;每个解决方案都提供特定的功能&#xff0c;共同帮助网络管理员完全控制其IT基础架构。网络软件工具提供监控网络运行状况、测量性能…

一个简单的MUX-VLAN实验(华为eNSP模拟器)

MUX-VLAN产生背景及用途 在数据中心网络中&#xff0c;数据中心管理员希望数据中心内部所有服务器&#xff08;或终端&#xff09;都可以访问外部网络&#xff0c;同时部分服务器之间可以互相通信&#xff0c;部分服务器之间相互隔离。一般情况下&#xff0c;为了实现所有服…