哈希思想的应用:位图、布隆过滤器及哈希切割

一.位图引入

给40定亿个不重复的无符号整数存储在文件中,如何判断一个数在不在其中?

分析:最容易想到的思路是将这些数字存储到某个能够实现快速查找的容器中,如红黑树或哈希表。

但是,10亿个字节大约占1G内存,那么40亿个整数如果想要在内存中存储需要16G空间。

故使用set(红黑树)或unordered_set(哈希表)等容器来存储是不现实的,主要原因就是内存不够。 

对于这种判断在不在的问题,不需要将数字直存储起来,而是用数字映射一个比特位,该bit标识在与不在两种状态即可。这样的数据结构,就叫做位图。

二.位图实现

  1. 非类型模版参数N,表示你想要开多少个bit的位图,这取决于数据的范围。如果数据是100以内的正整数,只需要开100bit;如果数据是unsigned int,需要开2^32个bit(512M内存)
  2. 数字的映射方法(哈希方法):直接定址法,数字x映射第x个比特位
  3. set接口:将x对应的比特位置为1,标识x在位图中
  4. reset接口:将x对应的比特位置为0,标识x不在位图中
  5. test接口:判断x是否在位图中,只需判断x对应的比特位是0还是1
template<size_t N>class bitset{private:vector<char> _bits;public:bitset(): _bits(N/8+1, 0){}void set(size_t x){size_t i = x / 8;size_t j = x % 8;//将_bits[i]的第j位置为1_bits[i] |= (1 << j);}void reset(size_t x){size_t i = x / 8;size_t j = x % 8;//将_bits[i]的第j位置为0_bits[i] &= ~(1 << j);}bool test(size_t x){size_t i = x / 8;size_t j = x % 8;if (_bits[i] & (1 << j)){return true;}return false;}};

三.位图和哈希表的比较 

  1. 相同点:二者都应用了哈希的思想,即用一个值映射另一个值的思想。二者都用值映射存储位置,只不过哈希表映射的每个存储单元较大,可以存储各种各样类型的数据,但位图映射的每个存储单元只有一个bit,存储的信息非常有限
  2. 不同点:哈希表将值存入了映射的存储空间,而位图没有。故哈希表可以处理哈希冲突(线性探测法或哈希桶),这是因为搜索时可以用值去和存储单元中的值进行比较。而位图不能发生哈希冲突,一个bit只能对应一个值,这也是我们采用直接定址法来映射的原因。

四.位图的应用 

1.给定100亿个整数,找只出现一次的整数

与开头的题目类似,不过这里要想办法表示一个数的三种状态,即出现0次,出现1次,出现2次以上,这需要两个bit才能标识。故可以使用两个位图。

00:第一个位图中x对应的bit位0,第二个位图中x对应的bit为0,表示x出现0次

01:第一个位图中x对应的bit位0,第二个位图中x对应的bit为1,表示x出现1次

10:第一个位图中x对应的bit位1,第二个位图中x对应的bit为0,表示x出现2次及以上

template<size_t N>class twobitset{private:bitset<N> _bs1;bitset<N> _bs2;public:void set(size_t x){if (_bs1.test(x) == false && _bs2.test(x) == false){//00->01bs2.test(x);}else if (_bs1.test(x) && _bs2.test(x) == true){//01->10;bs1.set(x);bs2.reset(x);}}bool test(size_t x){//01->出现1次return bs2.test(x) == true;}};

2.给两个文件,分别有100亿个整数,如何找两个文件的交集--交集中没有没有重复数字

方法一:分别将两个文件的数字放进两个位图,然后遍历每一个比特位,如果该比特位都是1,则对应的数字是交集 

方法二:先将一个文件中的数字放进位图,然后遍历另一个文件,看数字在不在位图中,如果在,则该数字是交集,同时将该数字对应的比特位置0,防止重复。

当文件中的数字个数大于2^32时,遍历比特位的方法更优,否则遍历文件的方法更优

五.布隆过滤器 

位图的缺点是只能只能映射整型,遇到字符串或者其它自定义类型无能为力。哈希表可以映射字符串,它是怎么做的呢?哈希表是先将字符串映射整型,然后再映射存储位置,做了两次哈希。

位图是否可以借鉴这种思路呢?答案是不可以,因为字符串无穷无尽,而整型是有限的,不管你怎么优化哈希方法,一定会存在多个字符串对应一个整型的情况,即使你再采用直接定址法照样会产生哈希冲突,而前面说了由于位图中没有把值存下来,所以不允许哈希冲突,否则判断时可能会出错。

而布隆过滤器直接另辟蹊径,既然哈希冲突可能会使判断出错,那么总有判断结果准确的时候,我不要求判断结果百分百正确,但是我可以想办法降低判断出错的概率。这就是布隆过滤器,它适用于允许判断出错的场景。

那么布隆过滤器是怎么降低出错概率的呢?

以string为例,布隆过滤器采用多种不同的哈希方法,将一string映射出多个整型值,然后再用整型值映射比特位(除留取余法)。这样一来,一个string对应多个bit,只有这几个bit都是1的时候才给出结论;string在位图中,当有一个bit为0时,给出string不在位图中的结论。

具体用几个哈希方法?哈希函数越多,映射的位就越多,误判率越低,但需要的空间就越多。这里直接给出结论,3个哈希函数比较合适,具体的数学推导有兴趣自行研究。

  1. 当给出在位图中的结论时,字符串不一定在位图中,因为这几个bit不一定是该字符串映射的。
  2. 当给出不在位图中的结论时,字符串一定不在位图中,因为没有字符串映射这几个bit

以上两点就是布隆过滤器的核心逻辑,也就是说,判断值不在,这个结论一定是准确的。因此,布隆过滤器才能发挥它的“过滤”功能。

template<size_t N, class K, class Hash1, class Hash2, class Hash3>class BloomFilter{private:bitset _bs;Hash1 _hf1;Hash2 _hf2;Hash3 _hf3;public:void set(const K& key){size_t hash1 = _hf1(key) % N;size_t hash2 = _hf2(key) % N;size_t hash3 = _hf3(key) % N;_bs.set(hash1);_bs.set(hash2);_bs.set(hash3);}bool test(const K& key){size_t hash1 = _hf1(key) % N;size_t hash2 = _hf2(key) % N;size_t hash3 = _hf3(key) % N;return _bs.test(hash1) && _bs.test(hash2) && _bs.test(hash3);}};

六.布隆过滤器的应用

一个典型的例子,当用户注册账号时,需要填写昵称。

可以先将数据库中的所有昵称放进布隆过滤器中,当用户输入昵称时,快速在布隆过滤器中查找昵称是否已经存在。若给出的结果是不在,则一定不在,提示用户昵称可用;若给出的结果是不在,则不能确定在不在,此时再去数据库中查找,以该结果为准,提示用户昵称是否可用。

布隆过滤器的意义在于过滤掉了那些一定不在数据库中的昵称,这样可以减少访问数据库的次数,从而提升效率。

七.布隆过滤器的删除

布隆过滤器原则上是不支持删除的,将一个字符串对应的多个比特位置0,可能会影响其它字符串。

 如图,删除str2就影响了str1,所以布隆过滤器原则上是不支持删除的。

如果想要支持删除,就要使用引用计数的方法,每一个存储单元不再只用一个bit来标识在与不在两种状态,而是用若干个bit作为一个存储单元,标识有多少个值映射到该存储单元。

例如三个bit作为一个存储单元,则该存储单元最多有8个值能映射,set就加1,reset就减1。当判断某个值在不在,只需判断它映射的若干个存储单元是否都不为0即可。

八. 哈希切割

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

近似算法:先将一个文件中的字符串放进布隆过滤器,布隆过滤器越大越好,这样可以减少哈希冲突。既然有1G空间,我们直接开2^33个bit(要用unsigned long long表示)。然后我们再遍历另一个文件,逐一判断字符串是否在布隆过滤器中。

精确算法怎么实现呢?位图只能放整型数据,哈希表,红黑树等又存不下。对此,我们的思路是分块处理,逐个击破,将文件分成若干个小份,不就可以加载进内存了吗?

如何分?平均分吗?分了等于没分,要找交集还是得遍历文件。

这里的问题在于要使的相同的字符串进入同一个文件,于是哈希思想的又一重大应用——哈希切割闪亮登场。

100亿个字符串,假定每个字符串50byte,则有500G大小。我们预计将每个大文件分为500个小文件,于是给每个文件编号:A0,A1,A2……A499,B0,B1,B2……B499。

分别遍历两个文件,所有字符串都用某个固定的哈希方法,映射成整型值,然后模上500,结果是多少,就存入几号文件。

相同的字符串映射出的整型值相同,因此会进入同一个文件。这样,我们就能对每个小文件单独处理了。例如将A0文件的字符串全部加载到内存,哈希表存储,遍历B0文件,逐一判断每个字符串是否在哈希表中。

这里还有一个细节,由于不是平均切割,某个文件可能会特别大,有两种情况。第一,可能是有很多重复的字符串,这并不影响,因为哈希表有去重功能,不会把这些重复的都存进去。第二,可能是有很多相似的字符串经过某种哈希方法映射到了相同文件,这时需要更换哈希方法,将该小文件再次切分成更小的文件。

综上,一开始可以不用考虑这个细节,直接读取文件存进哈希表,当内存不够时会抛出异常,我们只需捕获异常,然后再切分文件。

再看一个问题:

给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址? 与上题条件相同,如何找到top K的IP? 

解决方案:

哈希切割成若干个小文件,依次处理每个小文件,使用一个unordered_map/map统计ip出现的次数。

如果统计过程中出现抛内存异常,则说明单个文件过大,冲突太多,更换哈希函数,再次哈希切割这个小文件

如果没有抛异常,则正常统计,统计完一个小文件,记录最大的,释放内存,再统计下一个小文件。

如果要求次数最多的ip,则去记录的所有最大值中的最大值。如果要求top K,则建立K个元素的小堆,遍历记录的值来更新堆。最终留下的就是top K。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

知乎禁止转载的回答怎么复制做笔记?

问题 对于“禁止转载”的回答&#xff0c;右键复制是不行的&#xff0c;ctrl-c也不行&#xff0c;粘贴之后都是当前回答的标题。稍微看了代码&#xff0c;应该是对copy事件进行了处理。不过这样真的有用吗&#xff0c;真是防君子不防小人&#xff0c;只是给收集资料增加了许多…

IOS/安卓+charles实现抓包(主要解决证书网站无法打开问题)

安装 官网下载 https://www.charlesproxy.com/latest-release/download.do 安装charles文档 流程 上述链接解决下图问题 使用介绍 Charles介绍 上述链接看一至三即可&#xff0c;了解首页各个按钮的作用 charles全面使用教程及常见功能详解&#xff08;较详细&#xff09…

程序/进程替换(讲解)

本文旨在讲解进程替换的知识&#xff01;希望读完本文&#xff0c;能使读者对进程替换有更深一步的认识&#xff01;&#xff01;好的&#xff0c;废话不多说&#xff0c;干货来了&#xff01; 进程替换的引进&#xff01; 为什么要引进进程替换呢&#xff1f;我们创建子进程总…

(四)基于高尔夫优化算法GOA求解无人机三维路径规划研究(MATLAB代码)

一、无人机模型简介&#xff1a; 单个无人机三维路径规划问题及其建模_IT猿手的博客-CSDN博客 参考文献&#xff1a; [1]胡观凯,钟建华,李永正,黎万洪.基于IPSO-GA算法的无人机三维路径规划[J].现代电子技术,2023,46(07):115-120 二、高尔夫优化算法GOA简介 高尔夫优化算法…

C++作业2

自己封装一个矩形类(Rect)&#xff0c;拥有私有属性:宽度(width)、高度(height)&#xff0c; 定义公有成员函数: 初始化函数:void init(int w, int h) 更改宽度的函数:set_w(int w) 更改高度的函数:set_h(int h) 输出该矩形的周长和面积函数:void show() 代码&#xff1a…

js事件流与事件委托/事件代理

1 事件流 事件流分为两步&#xff0c;一是捕获&#xff0c;二是冒泡 1.1 捕获概念 捕获就是从最高层一层一层往下找到最内部的节点 1.2 冒泡概念 捕获到最小节点后&#xff0c;一层一层往上返回&#xff0c;像是气泡从最底部往上冒一样&#xff0c;由于水深不同压强不同&…

Git常用命令#更改用户名

1.查看当前用户信息 git config user.name2.更改用户名 特定仓库中更改用户名 如果你只想在特定仓库中更改用户名&#xff0c;可以在不使用 --global 参数的情况下执行相同的命令&#xff0c;并进入特定仓库的目录进行修改。 cd /path/to/your/repository git config user.na…

双通道 H 桥 5V 4A驱动芯片

SS6951A 为电机一体化应用提供一种双通道集成电机驱动方案。SS6951A 有两路 H 桥驱动&#xff0c;每个 H 桥可提供最大峰值电流 4.0A&#xff0c;可驱动两个刷式直流电机&#xff0c;或者一个双极步进电机&#xff0c;或者螺线管或者其它感性负载。双极步进电机可以以整步、2 细…

传统算法:使用 Pygame 实现选择排序

使用 Pygame 模块实现了选择排序的动画演示。首先,它生成一个包含随机整数的数组,并通过 Pygame 在屏幕上绘制这个数组的条形图。接着,通过选择排序算法对数组进行排序,动画效果可视化每一步的排序过程。在排序的过程中,程序找到未排序部分的最小元素,并将其与未排序部分…

基于YOLOv8深度学习的火焰烟雾检测系统【python源码+Pyqt5界面+数据集+训练代码】目标检测、深度学习实战

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

牛客算法题 HJ99 自守数 golang实现

题目 HJ99 自守数 描述 自守数是指一个数的平方的尾数等于该数自身的自然数。例如&#xff1a;25^2 625&#xff0c;76^2 5776&#xff0c;9376^2 87909376。请求出n(包括n)以内的自守数的个数数据范围&#xff1a; 1 ≤ &#xfffd; ≤ 100001≤n≤10000 输入描述&…

LeetCode(44)存在重复元素 II【哈希表】【简单】

目录 1.题目2.答案3.提交结果截图 链接&#xff1a; 存在重复元素 II 1.题目 给你一个整数数组 nums 和一个整数 k &#xff0c;判断数组中是否存在两个 不同的索引 i 和 j &#xff0c;满足 nums[i] nums[j] 且 abs(i - j) < k 。如果存在&#xff0c;返回 true &#xf…