数据结构-哈希-位图与布隆过滤器

位图与布隆过滤器

  • 一,位图
    • 题目分析
    • 位图设计
    • 位图代码
    • 经典题目
  • 二,布隆过滤器
    • 布隆过滤器概念
    • 布隆过滤器的插入
    • 布隆过滤器的结构
    • 布隆过滤器总结
    • 经典题目
  • 三,哈希切割

一,位图

题目分析

🚀给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。

🚀思路1:排序+二分查找
首先估算一下,40亿个整数的大小为16GB(10亿字节是1GB,40亿整数是4 * 10亿 * 4 字节 = 16GB)。显然,在内存中是完不成排序的,只能将这份数据分散到几个小文件中,然后利用归并排序,即使排序完成后二分查找也是非常困难的,因为二分查找是基于下标的随机访问,显然在文件中是不能随机访问的,所以只能每次局部加载一部分到内存中进行查找。可以看到经过上面的分析,过程是非常复杂且效率低的,如果是查找多个无符号整数,那这种方法更不能考虑。

🚀思路2:将数据存入到红黑树或者哈希表中进行查找
无论是哈希表还是红黑树,内部不仅要存储数据还有维护结点间关系的指针,光数据就要16GB,算上其他开销那就不仅仅是16GB了,要使用这种方法也只能是每次加载一部分进行查找,所以这种方法也不是很好。

🚀思路3:针对这个问题利用位图来解决绝对是再合适不过了,位图就是一种哈希结构,是一种直接定址法,将每个整数映射到一个固定的比特位,检查一个整数存不存在直接查看那个比特位是否为1即可,利用位图结构对于内存来说空间是绝对足够的,因为只需要42亿左右的比特位空间即可(因为无符号整数最大值为42亿多),大概只需要512MB左右的空间(40亿 / 8 = 5 亿字节 = 512 MB),并且位图结构查询效率极高,时间复杂度为O(1)。

下面,看下位图的设计。

位图设计

🚀位图最重要的三个方法:
set(将整数对应的比特位置1),
reset(将整数对应的比特位置0),
test(检测整数对应的比特位是否为1).

在这里插入图片描述
🚀我们用char的数组来模拟位图结构,如何定位一个整数映射到哪一个比特位呢?假设整数为N,第一步,用N / 8 得到N对应的比特位属于第几个char中。第二步,用N % 8得到N对应的比特位数据某个char中的第几个比特位。如果是用int数组模拟的话就是除32和模32。
🚀以13为例,13 / 8 = 1,说明13对应的比特位位于第1个char中(注意char是从0开始计数的),13 % 8 = 5,说明13对应的比特位是第一个char中的第五个比特位。就是上图中13指向的比特位。

template<size_t N> //N代表要开多少比特位class bitset{public:bitset(){_bits.resize(N / 8 + 1, 0);}void set(size_t N){}void reset(size_t N){}bool test(size_t N){}private:vector<char> _bits;};

🚀如何将某个比特位置为1:
在这里插入图片描述
例如将上图中绿色格子对应的比特位置为1,在将这一位置1的同时要保证不能破坏其他位的内容,所以只要将这一位 |= 1,其它位 |= 0即可(0 |= 0 还是0,1 |= 0 还是1)。

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

🚀如何将某个比特位置为0:
与将某个比特位置为1相反,那么用这个比特位 &= 0,其他比特位 &= 1即可(0 &= 1 = 0,1 &= 1 = 1)

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

🚀检测某个比特位是否为1:
假设N对应的比特位是属于某个char的第j位,那么将这个char右移j位再& 1,如果结果是1表示N对应的比特位为1,反之为0.

((_bits[i] >> j) & 1) == 1;

位图代码

namespace gy
{template<size_t N>class bitset{public:bitset(){_bits.resize(N / 8 + 1, 0);}void set(size_t N){size_t i = N / 8;size_t j = N % 8;_bits[i] |= (1 << j);}void reset(size_t N){size_t i = N / 8;size_t j = N % 8;_bits[i] &= (~(1 << j));}bool test(size_t N){size_t i = N / 8;size_t j = N % 8;return ((_bits[i] >> j) & 1) == 1;}private:vector<char> _bits;};
}

经典题目

  1. 给定100亿个整数,设计算法找到只出现一次的整数?

🚀上面实现的位图结构只能判断某个整数是否出现,不能判断整数出现了几次,但是对上面的结构稍加改造即可,上面的结构是一个整数映射一个比特位,我们可以让一个整数映射2个比特位进而来记录出现的次数。
00:表示没有出现,
01:表示出现了1次,
10:表示出现了两次及以上。
所以直接复用上面的结构即可:

template<size_t N>
class twobitset
{
public:twobitset(){_bs1.resize(N / 8 + 1, 0);_bs2.resize(N / 8 + 1, 0);}void set(size_t N){// 00->01if (_bs1.test(N) == false && _bs2.test(N) == false) {_bs2.set(N);}//01-> 10else if (_bs1.test(N) == false && _bs2.test(N) == true){_bs1.set(N);_bs2.reset(N);}}bool test(size_t N){return _bs2.test(N);}
private:bitset<N> _bs1; //表示较高的比特位bitset<N> _bs2; //表示较低的比特位
};
  1. 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?

🚀法1:将一个文件中的数据读入到位图结构中,在读取第二个文件的时候每读取一个数据就去位图中检测是否存在,如果存在说明这个数据就是交集,并且在位图中将这个整数对应的比特位置为0。(为了防止交集中出现重复的数据)

例如:第一组数据{1,3,6,8,4,7,2,5,74546,564,87};
第二组数据{2,2,5,6,7,3,3,9,9};
交集应该是{2,3,5,6,7}
如果没有将位图reset这一步的话得到的交集为{2,2,3,3,5,6,7};

🚀法2:分别将两个文件中的数据读入到两个位图中,然后遍历0-无符号整数最大值,如果某个整数N同时存在两个位图中,那么这个N就是交集,并且这种方法不会存在重复的问题。

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

🚀与问题1类似,本题就是找出出现1次或者2次的整数,我们还是将一个整数映射两个比特位,
00表示没有出现
01表示出现1次
10表示出现两次
11表示出现两次以上
所以只要将上面的set和test逻辑稍做修改即可:

void set(size_t N)
{//00->01if (_bs1.test(N) == false && _bs2.test(N) == false){_bs2.set(N);}//01->10else if (_bs1.test(N) == false && _bs2.test(N) == true){_bs1.set(N);_bs2.reset(N);}//10->11else if (_bs1.test(N) == true && _bs2.test(N) == false){_bs1.set(N);_bs2.set(N);}
}
bool test(size_t N)
{return _bs1.test(N) && !_bs2.test(N)  //10|| !_bs1.test(N) && _bs2.test(N);     //01
}

二,布隆过滤器

布隆过滤器概念

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

布隆过滤器的插入

🚀将某个字符串映射到某个比特位是不能直接实现的,因此需要使用字符串哈希函数将字符串转化为相应的整型,再将其映射到某个比特位。但是如果只是映射到某一个比特位的话,那么产生哈希冲突的概率就会很大,也就是说会产生“误判”,假设下面这种情况,hello存在的信息并没有记录在位图结构中,百度存在的信息存储在了位图结构中,但此时去检测hello是否存在时,就会发生“误判的情况”。
在这里插入图片描述
🚀这种“误判”是无法避免的,但是要尽量减少,所以通常一个字符串会被映射到多个比特位上(根据需而定,本文采用三个)。所以一个字符串要根据多个哈希函数转化出的多个整数来映射到多个比特位上。

在这里插入图片描述

布隆过滤器的结构

template<size_t N,typename K = std::string,
typename Hash1 = BKDRHash,
typename Hash2 = APHash,
typename Hash3 = DJBHash>
class BloomFilter
{
public:void set(const K& key){}void test(const K& key){}
private:const int _rate = 5;bitset<N * _rate> _bs;
};

🚀布隆过滤器的大多数使用场景都是针对字符串的,所以模板参数K给的缺省参数就是string,并且对应的三个Hash函数都是字符串Hash函数。
🚀非类型模板参数不再代表开多少比特位,而是代表要存储多少个K类型的对象,N的个数和哈希函数的个数能够推断出具开多少比特位是较为合适的,参考博客: 布隆过滤器。

template<size_t N,typename K = std::string,
typename Hash1 = BKDRHash,
typename Hash2 = APHash,
typename Hash3 = DJBHash>
class BloomFilter
{
public:void set(const K& key){size_t len = N * _rate;size_t hash1 = Hash1()(key) % len;size_t hash2 = Hash2()(key) % len;size_t hash3 = Hash3()(key) % len;_bs.set(hash1);_bs.set(hash2);_bs.set(hash3);}bool test(const K& key){size_t len = N * _rate;size_t hash1 = Hash1()(key) % len;size_t hash2 = Hash2()(key) % len;size_t hash3 = Hash3()(key) % len;//有一个位置不是1就表示不存在if (_ba.test(hash1) == false) return false;if (_ba.test(hash2) == false) return false;if (_ba.test(hash3) == false) return false;return true;}
private:const int _rate = 5;bitset<N * _rate> _bs;
};

🚀布隆过滤器一般是不支持删除的,因为删除一个元素时,可能会影响到其他元素。可以通过给位图结构中的比特位扩展成一个小的计数器,由原来的判断是否存在0还是1,转化为出现的次数。在插入元素的时候由哈希函数计算出的k个整型值对应的k个比特位做+1操作,相应删除时做-1操作。
如果为每个比特位增加一个引用计数的话,可能会引发计数回绕的问题。

🚀例如,由原来1个位置只有1个比特位,转化位1个位置存在3个比特位(能表示0 - 7),在插入的时候:
000->001
001->010
010->011
011->100
100->101
101->110
110->111
删除时做相反操作,这样就能支持基本的删除操作了。但这种做法仍然存在缺陷,因为判断一个元素存在是不准确的,那么代表着删除某个不存在的元素就势必会影响到真正存在的元素。

布隆过滤器总结

🚀使用布隆过滤器判断一个元素是否存在时,如果检测到一个元素不存在那么代表真的不存在,如果检测到一个元素存在是不准确的存在“误判“的情况。

🚀布隆过滤器的优点:

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

🚀布隆过滤器的缺点:

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

🚀布隆过滤器适用场景:
比如我们在注册某个网站时要输入一个昵称,电话号码,邮箱等等,往往是我们输入昵称后系统就会返回一个结果告诉我们这个昵称是否被别人使用过,这就是布隆过滤器典型的应用场景,因为如果一个昵称不存在,那就是真的不存在用户可以使用这个昵称。而,一个昵称显示存在的时候,其实它不一定真的存在但用户是感知不到的,也就是说这种场景下的误判是可接受的。
对于电话号码和邮箱这种,用户是清楚自己的手机号还有邮箱是否存在注册记录的,所以对于手机号这种出现误判是不能接受的,对于这种场景起到的是过滤的作用,如果手机号或邮箱不存在那么就是真的不存在,返回给用户电话号码或者邮箱可用。如果检测手机号或邮箱存在,那么再去数据库中查询,看是否真正的存在。可以减少数据库的访问提高效率,这种场景起到的是过滤作用。

经典题目

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

🚀近似算法就可以利用布隆过滤器来完成,首先将一个文件的query的数据存入一个布隆过滤器中,再依次读取出另一个文件中的query去布隆过滤器中检测是否存在,如果存在就是交集。

三,哈希切割

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

🚀由于是找到交集的精确算法就不能使用布隆过滤器这种位图结构来完成了。假设每个query的大小是50字节,那么100亿个query就是5000亿字节 = 500GB。500GB肯定是不能同时加载到内存中的,所以要对其进行切割,假设切割1000份每份为500MB。

在这里插入图片描述
如果采用平均切割的方式,那么在寻找交集的过程中就要进行暴力的匹配,任何两个Ai小文件,Bi小文件都要进行依次匹配,这样的效率的很低的。所以有人提出哈希切割这种方式,对于大文件中的每个query都先经过哈希函数进行计算,将计算的结果模1000,得到的结果就是哪个小文件的下标,对于A,B文件来说,如果是相同的query,那么经过同一个哈希函数必定会映射到相同下标的小文件中,于是只需两个下标相同的小文件求交集,最终再汇总即可。

🚀但是,由于不是平均切割,就可能会造成某个小文件的体积过大已经超过内存的大小,那么它就不能被加载到内存中,后续的工作就不能继续运行。单个小文件体积过大有两种可能:
1,文件中存在大量重复的query。
2,文件中有大量不冲突的query。
针对这两种情况,我们可以将这个小文件加载到红黑树或者哈希表中,如果在加载的过程中,出现了抛出了内存的异常,那么就表示为情况2,此时就需要换一个新的哈希函数对此小文件继续切割。如果在加载过程中并没有出现异常那么就表明为情况1,那么直接依次加载下标相同的另一个小文件的query,在红黑树或者是哈希表中查找是否存在,如果存在就是交集。

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

🚀此问题与上个问题的解法类似,都是采用哈希切割的方式,100GB的文件哈希切割成若干份,但是同样会出现某个小文件的体积过大的问题:
1,文件中存在大量重复的IP。
2,文件中有大量不冲突的IP。
解决方法也是类似的,由于本题是统计出出现次数最多的IP地址,那么就将小文件加载到map中统计次数,如果加载成功没有抛出内存的异常表示为情况1,直接找出次数最多的即可。如果在加载过程中抛出内存异常,那么就需要换一个哈希函数继续切割此文件。

🚀如果是找到TopK个IP,只需建立一个大小为K的小堆即可,将每个IP出现的次数与堆顶元素比较,如果比堆顶元素大那么就进堆,这样就可以找到出现次数TopK的IP。

🚀如果用Linux指令来切割的话,使用sort指令配合uniq指令就能完成。首先使用sort对文件进行排序,在使用uniq进行去重(uniq只能处理相邻文本),uniq指令搭配-c选项使用,- c:显示出重复出现的次数,最终在使用依次sort -nr指令,
-n :依照数值大小排序,
-c:按降序方式排序。
如果是取出现次数最多的数据再配head -1 指令即可。如果是取TopK的数据 搭配head -K 即可。如果需要将得到的字符串再保存到文件中直接重定向到指定文件即可。

sort test.txt | uniq -c | sort -nr | head -1

下面是随便造的一些字符串。对上面的指令做测试使用。

在这里插入图片描述
🚀测试取出现次数最多的字符串

sort test.txt | uniq -c | sort -nr | head -1

在这里插入图片描述
🚀取出现次数前三多的数据。

sort test.txt | uniq -c | sort -nr | head -3

在这里插入图片描述

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

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

相关文章

设计模式之策略模式笔记

设计模式之策略模式笔记 说明Strategy(策略)目录策略模式示例类图抽象策略类策略A类策略B类策略C类促销员类测试类 说明 记录下学习设计模式-策略模式的写法。JDK使用版本为1.8版本。 Strategy(策略) 意图:定义一系列的算法&#xff0c;把它们一个个封装起来&#xff0c;并且…

Uncaught TypeError: Illegal invocation

使用console.time报错 console.time将在当前对象(即支持)的上下文中执行&#xff0c;发现一些老的chrome版本中&#xff0c;上下文可能有问题. 解决&#xff1a; 可以使用 console.time.call(window,1111)

把网页地址中的url的参数转化为obj

1.例子: var str"http://www.hqyj.com/index.html?uid123&page19&dt20230407&namekaren" 2.代码: var str"http://www.hqyj.com/index.html?uid123&page19&dt20230407&namekaren"function fn (str) {var arr1str.split("…

opencv编译

文章目录 一、编译前工作二、编译安装1、Windows2、Linux 一、编译前工作 进入下载页面https://github.com/opencv/opencv&#xff0c;下载指定.tar.gz源码包&#xff0c;例如&#xff1a;opencv-4.7.0.tar.gz。解压到指定目录。 二、编译安装 opencv构建时&#xff0c;需要…

Visual C++中*号位置和功能详细解说

我是荔园微风&#xff0c;作为一名在IT界整整25年的老兵&#xff0c;今天来聊聊Visual C中*号的位置。 我知道在程序员队伍中有一群特别细心、谨慎的可爱的人&#xff0c;他们经常为一些在别人看来小的不能再小的问题所困惑。比如说&#xff0c;*号的位置&#xff0c;让很多人…

Appium安装部署

目录 一、检查Java环境 二、安装android SDK 一、检查Java环境 Android SDK依赖ava环境&#xff0c;因此需要先安装jdk。在CMD中输入java -version 出现下图的结果&#xff0c;说明当前环境已安装jdk 如果提示java命令无效&#xff0c;请安装后进行下一步。 二、安装androi…

ModaHub AI模型社区:向量数据库CPU 版 Milvus和GPU 版 Milvus 版本比较

目录 CPU 版 Milvus 版本比较 概述 CPU 版 Milvus 支持的索引类型 浮点型向量 二值型向量 GPU 版 Milvus 版本比较 概述 GPU 版 Milvus 支持的索引类型 浮点型向量 二值型向量 CPU 版 Milvus 版本比较 概述 Milvus 提供两个发行版本&#xff1a;CPU 版本和 GPU 版本…

Unix/Linux编程:UDS 流(Stream)

〇、前言 socket 是一种 IPC &#xff08;Inter-Process Communication&#xff0c;进程间通信&#xff09;方法&#xff0c;它允许位于同一主机&#xff08;计算机&#xff09;或使用网络连接起来的不同主机上的应用程序之间交换数据。通过使用Socket&#xff0c;开发人员可以…

解决不允许一个用户使用一个以上用户名与一个服务器或共享资源的多重连接的问题

问题概述&#xff1a; 用windows server 2012 r2 vl x64搭了个文件服务器&#xff0c;在使用时有个问题&#xff0c;老是用户登录有问题&#xff0c;提示“不允许一个用户使用一个以上用户名与一个服务器或共享资源的多重连接”。出现的原因不详&#xff0c;网上也没查到合理的…

路由器的工作原理详解

什么叫路由&#xff1f; 路由器的英文是 Router&#xff0c;也就是「找路的工具」。找什么路&#xff1f;寻找各个网络节点之间的路。 换句话说&#xff0c;路由器就像是快递中转站&#xff0c;包裹会经过一个个的中转站&#xff0c;从遥远的地方寄到你家附近&#xff0c;数据…

基于深度学习的高精度袋鼠检测识别系统(PyTorch+Pyside6+YOLOv5模型)

摘要&#xff1a;基于深度学习的高精度袋鼠检测识别系统可用于日常生活中或野外来检测与定位袋鼠目标&#xff0c;利用深度学习算法可实现图片、视频、摄像头等方式的袋鼠目标检测识别&#xff0c;另外支持结果可视化与图片或视频检测结果的导出。本系统采用YOLOv5目标检测模型…

【软件开发】MyBatis 理论篇

MyBatis 理论篇 1.MyBatis 是什么&#xff1f; MyBatis 是一个半 ORM&#xff08;对象关系映射&#xff09;框架&#xff0c;它内部封装了 JDBC&#xff0c;开发时只需要关注 SQL 语句本身&#xff0c;不需要花费精力去处理加载驱动、创建连接、创建 statement 等繁杂的过程。…