花未全开月未圆:布隆过滤器的留白美学
目录
一、布隆过滤器的引入
二、布隆过滤器
三、布隆过滤器的模拟实现
四、哈希切割
一、布隆过滤器的引入
在上文中 请点击这里,是判断整形类型的值在不在位图中,如果我们要判断字符串呢?判断字符串存不存在,整形可以直接映射到一个比特位,但字符串呢? 所以我们可以用一个字符串转换为整形的算法 :
template<class T>
struct BKDRHash
{size_t operator()(const T& str){size_t hash = 0;for (auto ch : str){hash = hash * 131 + ch;}return hash;}
};
把它转化成整形存在位图中 ,但是这里会出现问题 ,举个例子,下面有三个字符串,分别是"QQ" 和"微信","微软",他们通过算法转化成整形分别对应到该对应的比特位
这里,"QQ","微信" ,"微软" 都转化为了整形数字 : 20 ,38 ,59 此时如果有个" 网易 "" 网易 "这个字符串,他的整型值可能跟"微信"它的值不一样 (也可能相同),但是通过%了一下之后(因为空间肯定是不能无限开的,要把他们限制在这个空间的大小内),可能值就相同 ,可能也映射到同一个比特位 ,网易是不在这个位图里的,但是我们通过位图判断的时候发现他在这个位置上也显示为1(1是因为这个位置是 "微信" 对应的位置),这里就会产生误判
也就是说,当字符串这个情况要用到位图的时候,可能会存在冲突,导致误判,其中在是可能准确的,但是不在是一定准确的,我们要利用这个特性 ,那我们采取什么方式呢?
这里有个叫布隆的人,想到一种方式 ,他发现不能让误判率消失,但是可以让ta尽可能的降低,那么用什么方式来降低呢? : 让一个字符串用多个不一样的算法映射出多个不一样的整型出来,每一个整型 的 比特位都为置1 ,在检测的时候要检测这几个位置,除非这几个位置都为一,那么才表示这个字符串存在,如果有一个不为一,那就说明这个字符串是不存在的
这就像是在火车站接人,一批乘客出来之后,你要找到对应的人,你需要他提供他的外貌特征,比如说衣服是什么颜色,裤子是短的还是长的,带没带眼镜等等 ,所以这里的特性就是增加它的识别的点
比如说在这里,我一个字符串,我映射出三个位置,你一个字符串与这三个位置的比特位都是 1 的时候,我们认为它存在,但其实也可能是不存在的,但是这个概率就会大大降低了 ,当然,在这里面位置也可能会交叉 ,有交叉不影响的 , 判定条件是三个位置都为一 才表示这个字符串是存在的
此时,如果" 网易 "跟"微信"搞混的概率就大大减小 ,这样就大大降低了误判率,注意误判是无法消除误判的
只要有一个位置是不在的,那么,这个字符串是一定不在这个位图中的 ,这种方式叫做布隆过滤器
二、布隆过滤器
布隆过滤器:布隆过滤器是由布隆在1970年提出的一种紧凑型的,比较巧妙的概念型数据结构,特点的高效的插入和查询,它可以用来告诉你“某种东西一定不存在或者可能存在”。它是利用多个哈希函数(字符串转整型) ,将一个数据映射到位图结构中去,此方法提高查询的效率,同时也节约大量的内存空间
布隆过滤器是一种空间效率极高的概率型数据结构,用于快速判断一个元素“一定不在” 或 “可能在” 一个集合中 ,它不会存储实际数据,因此非常适合大数据场景下的去重、缓存穿透保护等问题 ,减低数据库查询负载压力提高效率
请简单的讲一下布隆过滤器的应用场景
- 准确的判断一个数据不在,但对于数据存在的情况存在一定的误判,对于一些场景,例如判断游戏id是否注册过 。如果 ,用户输入的id是不存在 ,那么可以准确的判断用户输入的id是不存在的,用户是可以进行注册id的,但对于部分用户输入的id实际上不存在,但是会误判为存在 ,也就是在的结果是存在误判的,不在的结果是不存在误判的
- 布隆过滤器,具有过滤的作用,对于用户要求得到准确的在或者不在的场景,使用布隆过滤器可以过滤一定不存在的情况,例如对于用户一些数据库查询请求,数据是放到数据库中的,由于布隆过滤器的内存占用小,所以数据我们可以提前存放到布隆过滤器中,当用户进行查询的时候,如果数据是不存在的 , 那么用布隆过滤器可以更快的反馈给用户,对于使用布隆过滤器判断存在的情况,(判断存在是存在误判的 )所以并不能直接反馈给用户,要去数据库上进行查询这个数据是否存在,将数据库的准确查询结果反馈给用户,这样可以一定程度避免数据库拥堵,卡顿,因为对于一定不存在的情况使用布隆过滤器就可以判断出来反馈给用户,布隆过滤器的查询的时间复杂度是O(1)级别的,十分高效,那么此时数据库就只需要判断布隆过滤器判断为存在的不准确数据即可,这样就极大的降低了数据库的查询负载压力,提高了效率
三、布隆过滤器的模拟实现
- 这里实现的布隆过滤器是针对字符串的,对于哈希函数(Hashfunc) ,我们需要使用三个字符串转整形的算法来替代,因为同一个字符串使用三个不同的字符串转整形算法算出的整形大多不相同,算出三个整形后使用同一个数字取模 ,得到对应的哈希地址
- 三个 字符串转整形 的算法分别是BKDRHash算法,APHash算法,DJBHash算法
template<class T>
struct BKDRHash
{size_t operator()(const T& str){size_t hash = 0;for (auto ch : str){hash = hash * 131 + ch;}return hash;}
};template<class T>
struct APHash
{size_t operator()(const T& str){size_t hash = 0;for (size_t i = 0; i < str.size(); i++){size_t ch = str[i];if ((i & 1) == 0){hash ^= ((hash << 7) ^ ch ^ (hash >> 3));}else{hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));}}return hash;}
};template<class T>
struct DJBHash
{size_t operator()(const T& str){size_t hash = 5381;for(auto ch : str){hash += (hash << 5) + ch;}return hash;}
};
- 按我们上面的结论来说,我们一个字符串去映射尽可能多的值,这样不会更减少误判率吗?那是不是可以选更多的哈希函数呢?其实不是的,选太多的哈希函数去映射出更多的值会大大增加空间的负担 ,空间的消耗就会变多
映射出多个位置,那么怎么判断呢?
- 一个字符串传给这三个哈希函数生成三个整型值 : hash1,hash2,hash3 ,三个整型值都用内部的位图的test去测试一下,如果有一个值在对应的比特位上面为零,那么说明该字符串是一定不存在的,只有在这三个位置上都是1才算是存在,(当然大概率存在,它是不一定的,因为存在误判)
那能去支持reset函数吗?
- 不能!!! reset是让一个比特位上面的一变成零,可以做到让原本存在的一个字符串,把ta那三个映射位置上的一个位置 , 或者全部位置的一变为零,从而表示让这个值不存在,但是如果有某些字符串,它的映射位置是与删除的字符串的映射位置是相同的,那么这个位置上的一也变成了零,根据判断的原理,但凡有一个映射位置的值在这里为零,那么都会判断为不存在,所以删除一个值,其他值也会受到影响,把存在的值反而变成不存在的,所以我们这里不支持reset
怎么支持set?
- set : 让一个字符串表示在位图中(存在) ,通过三个哈希函数算出的三个哈希地址,调用底层的位图_bs将算出的哈希地址(比特位)都设置为1
#pragma once
#include<bitset>
#include<string>template<class T>
struct BKDRHash
{size_t operator()(const T& str){size_t hash = 0;for (auto ch : str){hash = hash * 131 + ch;}return hash;}
};template<class T>
struct APHash
{size_t operator()(const T& str){size_t hash = 0;for (size_t i = 0; i < str.size(); i++){size_t ch = str[i];if ((i & 1) == 0){hash ^= ((hash << 7) ^ ch ^ (hash >> 3));}else{hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));}}return hash;}
};template<class T>
struct DJBHash
{size_t operator()(const T& str){size_t hash = 5381;for (auto ch : str){hash += (hash << 5) + ch;}return hash;}
};template<size_t N, class k=string,class Hash1= BKDRHash<k>,class Hash2=APHash<k>,class Hash3=DJBHash<k>>
class BloomFilter
{
public://一个值映射多个位置 ,key :要映射的值void set(const string& key){size_t hash1 = Hash1()(key)%N;//这里 Hashi() 是Hashi的匿名对象_bs.set(hash1);size_t hash2 = Hash2()(key)%N;_bs.set(hash2);size_t hash3 = Hash3()(key)%N;_bs.set(hash3);}bool test(const string& key)// 检测key的三个映射位置,一旦有一个不是,则返回false
{size_t hash1 = Hash1()(key)%N;if (!_bs.test(hash1)){return false;}size_t hash2 = Hash2()(key)%N;if (!_bs.test(hash2)){return false;}size_t hash3 = Hash3()(key)%N;if (!_bs.test(hash3)){return false;}return true;
}
private:bitset<N> _bs;
};
- 这里 Hashi() 是Hashi的匿名对象 ,也可以写成这样
void set(const string& key)
{Hash1 H1; Hash2 H2; Hash3 H3;size_t hash1 = H1(key)%N;_bs.set(hash1);size_t hash2 = H2(key)%N;_bs.set(hash2);size_t hash3 = H3(key)%N;_bs.set(hash3);
}bool test(const string& key)// 检测key的三个映射位置,一旦有一个不是,则返回false
{Hash1 H1; Hash2 H2; Hash3 H3;size_t hash1 = H1(key)%N;if (!_bs.test(hash1)){return false;}size_t hash2 = H2(key)%N;if (!_bs.test(hash2)){return false;}size_t hash3 = H3(key)%N;if (!_bs.test(hash3)){return false;}return true;
}
test一下:
void TestBloomFilter1()
{BloomFilter<100> br;br.set("美羊羊");br.set("喜羊羊");br.set("懒羊羊");cout << br.test("沸羊羊") << endl;cout << br.test("美羊羊") << endl;cout << br.test("喜羊羊") << endl;cout << br.test("懒羊羊") << endl;
}
测试布隆过滤器对相似数据和不相似数据的误判率
-
布隆过滤器的存储空间N和实际字符串的个数n,通常是N是n的四倍多,即空间要开字符串个数的四倍,通常来讲,空间越大,误判率越低,但是误判不能消除,一般我们使用布隆过滤器的就是为了节省空间,一味的扩大空间,使用布隆过滤器的意义就没有了
#include "BloomFilter.h"void TestBloomFilter2()
{const size_t N = 10000;bloomfilter<N * 4> bf;string str1 = "https://blog.csdn.net/2402_87310323";for (int i = 0; i < N; i++){bf.Set(str1 + to_string(i));}size_t sum1 = 0;for (int i = 10000; i < 2 * N; i++)//相似字符串{if (bf.Test(str1 + to_string(i))){sum1++;}}cout << "相似字符串的误判率是:" << (double)sum1 / N << endl;//不相似字符串size_t sum2 = 0;string str2 = "星期一";for (int i = 10000; i < 2 * N; i++){if (bf.Test(str2 + to_string(i))){sum2++;}}cout << "不相似字符串的误判率是:" << (double)sum2 / N << endl;
}int main()
{//TestBloomFilter1();TestBloomFilter2();return 0;
}
四、哈希切割
题目:给两个文件,分别由100亿个query(查询) ,query查询语句是字符串,一个字符串的大小大约有30byte,我们只有1G内存,如何找出两个文件的交集?给出精确算法和近似算法
100亿个query有多大,query查询语句是字符串,一个字符串的大小大约有30byte,那么100亿个query大约就是3000亿byte,1G大约等于10亿字节byte,那么3000亿byte就是有300G
精确算法
要使用精确算法的话,我们就不能有误判,所以说这里不能用布隆过滤器,现在要找交集,那首先要去重,所以说我们可以用set处理 ,由于内存只有1g,两个文件,那肯定要用两个set,所以平均一个set,只要用500M的内存,我们需要把文件平均分,分成600份,每个小文件就是500M,这样才能满足我们的需求,但是我们平均分成500份,我们要找出交集,那样的话,在A文件中,一个A的小文件要和B文件中所有小文件依次放入set中来找到交集 ,效率太低下了
所以这里我们要用哈希切割
哈希切割就是用哈希函数给每一个字符串一个值并且%600,不能让ta超过600份,相同的query(字符串)使用了同一个哈希函数进行计算,那么就会分到同一个小文件中,那么一个小文件中就有对应关系 只需要让两个大文件中相同i的小文件去找交集就行
进入第i个小文件的情况,这里面i都是相同的
- 相同的字符串,所以i相同在各自文件的第i个小文件
- 字符串不相同, i 算出来是相同的,在各自文件的第i个小文件
能进入第i个小文件 ,只有这两种情况 ,所以我们只用去分别两个文件对应的i个文件中去找,没必要去全部去找,减小了查找的范围
但这里还有个问题,如果太多的 i 都相同了,会导致其中一个小文件的内存非常大
首先有两种情况
- 第一种情况是字符串相同的很多
- 第二种情况是字符串大多数都不相同
首先,把A的小文件中的字符串读入到set中,再读B的第i个小文件 , 如果set的insert报错抛异常,(也就是内存不够了,如果是大量相同的,那么就不会Insert进去),就说明大多数的字符串都是冲突(大多数都不相同)的, 如果能读出来insert到set中,说明A的第i个小文件与B的第i 个小文件中有大量的相同的字符串,如果抛异常了,我们要捕获异常,说明有大量冲突,内存存不够,肯定要再换一个哈希函数再进行第二次哈希切分
近似算法
用布隆过滤器进行处理,布隆过滤器可以标记字符串是否存在的状态,布隆过滤器判断不存在是精确的,判断数据的存在是误判的,所以使用布隆过滤器是不精确的
判断数据的存在用精确算法进行处理即可,只不过是将set换成布隆过滤器进行标记
题目:给一个超过100G大小的log file,log存着IP地址,如何设计算法找到出现次数最多的IP地址?
同样使用哈希切割,将100G的文件切分成200份小文件,一个小文件大小约500MB,要用map才能统计小文件中相同IP的次数,map中的key是为字符串存IP,value是int即IP出现的次数,同时每次更新, 两个变量一个是出现最多的次数count类型是int,一个是count对应的字符串 str 类型是string,这里也可能会出现一个小文件的内存太大,这时候使用类似于上面题目中的精确算法中的set使用抛异常加进行二次切割,当每次将小文件的数据ip依次放入map中后,看一下放入数据ip后map中的ip对应的出现次数是否大于int count,如果大于那么更新count为ip对应map中的次数,同时更新str为ip即可,当遍历完成所有小文件后,那么str就为出现次数最多的IP地址