四、布隆过滤器
日常生活中,包括在设计计算机软件时,我们经常要判断一个元素是否在一个集合中,比如在字处理软件中,需要检查一个英语单词是否拼写正确(也就是要判断它是否在已知的字典中);在 FBI,一个嫌疑人的名字是否已经在嫌疑名单上;在网络爬虫里,一个网址是否被访问过等等。最直接的方法就是将集合中全部的元素存在计算机中,遇到一个新元素时,将它和集合中的元素直接比较即可。
一般来讲,计算机中的集合是用哈希表(hash table)来存储的。它的好处是快速准确,缺点是费存储空间。当集合比较小时,这个问题不显著,但是当集合巨大时,哈希表存储效率低的问题就显现出来了。
比如说,一个像 Yahoo、Hotmail 和 Gmail 那样的公众电子邮件(email)提供商,总是需要过滤来自发送垃圾邮件的人(spammer)的垃圾邮件。一个办法就是记录下那些发垃圾邮件的 email 地址。由于那些发送者不停地在注册新的地址,全世界少说也有几十亿个发垃圾邮件的地址,将他们都存起来则需要大量的网络服务器。
如果用哈希表,每存储一亿个 email 地址,就需要 1.6GB 的内存(用哈希表实现的具体办法是将每一个 email 地址对应成一个八字节的信息指纹,然后将这些信息指纹存入哈希表,由于哈希表的存储效率一般只有 50%,因此一个 email 地址需要占用十六个字节。一亿个地址大约要 1.6GB,即十六亿字节的内存)。因此存储几十亿个邮件地址可能需要上百 GB 的内存。除非是超级计算机,一般服务器是无法存储的。
- 用哈希表存储用户记录,缺点:浪费空间。
- 用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理了。
- 将哈希与位图结合,即布隆过滤器。
0、布隆过滤器概念
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你**“某样东西一定不存在或者可能存在”**,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
1、布隆过滤器查找
时间复杂度与哈希函数的个数成正比也就是O(K),K位哈希函数的个数。
3、布隆过滤器模拟实现
import java.util.BitSet;
class SimpleHash {
public int cap;//当前容量
public int seed;//随机
public SimpleHash(int cap,int seed) {
this.cap = cap;
this.seed = seed;
}
//根据seed不同 创建不能的哈希函数
int hash(String key) {
int h;
//(n - 1) & hash
return (key == null) ? 0 : (seed * (cap-1)) & ((h = key.hashCode()) ^ (h >>> 16));
}
}
public class MyBloomFilter {
public static final int DEFAULT_SIZE = 1 << 20;
//位图
public BitSet bitSet;
//记录存了多少个数据
public int usedSize;
public static final int[] seeds = {5,7,11,13,27,33};
public SimpleHash[] simpleHashes;
public MyBloomFilter() {
bitSet = new BitSet(DEFAULT_SIZE);
simpleHashes = new SimpleHash[seeds.length];
for (int i = 0; i < simpleHashes.length; i++) {
simpleHashes[i] = new SimpleHash(DEFAULT_SIZE,seeds[i]);
}
}
/**
* 添加元素 到布隆过滤器
* @param val
*/
public void add(String val) {
//让X个哈希函数 分别处理当前的数据
for (SimpleHash simpleHash : simpleHashes) {
int index = simpleHash.hash(val);
//把他们 都存储在位图当中即可
bitSet.set(index);
}
}
/**
* 是否包含val ,这里会存在一定的误判的
* @param val
* @return
*/
public boolean contains(String val) {
//val 一定 也是通过这个几个哈希函数去 看对应的位置
for (SimpleHash simpleHash : simpleHashes) {
int index = simpleHash.hash(val);
//只要有1个为 0 那么一定不存在
boolean flg = bitSet.get(index);
if(!flg) {
return false;
}
}
return true;
}
public static void main(String[] args) {
MyBloomFilter myBloomFilter = new MyBloomFilter();
myBloomFilter.add("hello");
myBloomFilter.add("hello2");
myBloomFilter.add("bit");
myBloomFilter.add("haha");
System.out.println(myBloomFilter.contains("hello"));
System.out.println(myBloomFilter.contains("hello3"));
System.out.println(myBloomFilter.contains("he"));
}
}
package org.example.bloomfilterdemo;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
/**
* @Author huis
*/
public class Test {
private static int size = 1000000;//预计要插入多少数据
private static double fpp = 0.001;//期望的误判率
private static final BloomFilter<Integer> BLOOM_FILTER
= BloomFilter.create(Funnels.integerFunnel(), size, fpp);
public static void main(String[] args) {
//插入数据
for (int i = 0; i < 1000000; i++) {
BLOOM_FILTER.put(i);
}
int count = 0;
for (int i = 1000000; i < 2000000; i++) {
if (BLOOM_FILTER.mightContain(i)) {
count++;
}
}
System.out.println("总共的误判数:" + count);
}
}
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
</dependencies>
4、 布隆过滤器删除
布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。比如:删除上图中"tencent"元素,如果直接将该元素所对应的二进制比特位置0,“baidu”元素也被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。
**一种支持删除的方法:**将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。这种方法的缺陷:
- 无法确认元素是否真正在布隆过滤器中【会有误判】
- 存在计数回绕【回绕意思为:溢出】
5、 布隆过滤器优点
- 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
- 哈希函数相互之间没有关系,方便硬件并行运算
- 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
- 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
- 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
- 使用同一组散列函数的布隆过滤器可以进行交、并、差运算;
6、布隆过滤器缺陷
- 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)
- 不能获取元素本身
- 一般情况下不能从布隆过滤器中删除元素
- 如果采用计数方式删除,可能会存在计数回绕问题
7、布隆过滤器使用场景
- google的guava包中有对Bloom Filter的实现
- 网页爬虫对URL的去重,避免爬去相同的URL地址。
- 垃圾邮件过滤,从数十亿个垃圾邮件列表中判断某邮箱是否是垃圾邮箱。
- 解决数据库缓存击穿,黑客攻击服务器时,会构建大量不存在于缓存中的key向服务器发起请求,在数据量足够大的时候,频繁的数据库查询会导致挂机。
- 秒杀系统,查看用户是否重复购买。
8、 海量数据类型面试题
0、哈希切割
- **给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址? 与上题条件相同, 如何找到topK的IP? **
**答:**如果忽略大小,我们可以统计每个IP出现的次数。我们可以使用<K,V>结构来解决这个题。但是问题目前是100G的数据太大了,一次性是无法加载到内存当中的。
思路:尝试把当前这1个文件给拆分成若干个小文件。问题是如何拆分?如果按照大小将100G进行均分,会出现一个情况,一个文件当中最多的IP地址,不一定就是整体上最多的IP地址。所以,均分解决不了。
哈希切割:将相同的IP字符串存储到同一个文件(文件组或文件夹)当中去。
a、将IP字符串转化为整数
b、文件下标 = hash(IP),将IP存储到对应的小文件中去
c、读取每个文件的内容,统计每个IP出现的次数
1、位图应用
- 给定100亿个整数,设计算法找到只出现一次的整数?
**方法一:哈希切割:**将相同数字哈希切割到同一个文件中去,遍历每个文件,统计每个数字出现的次数,此时内存中就可以知道那些数字只出现了一次。
**方法二:两个位图: **若bitSet1[num]=1,bitSet1[num]=0,则说明num只出现了一次
**方法三:一个位图:**使用两个比特位从当小型计数器,统计数字出现的个数
- 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件的交集?
方法一:哈希切割
对两个大文件分别进行哈希切割,切割若干小文件到两个文件夹中(此过程可以顺手对文件排序),然后遍历两个文件夹中的文件求出交集。
方法二:位图
将两个文件的数据添加到用两个位图中,为两个位图做按位与操作就可以求出交集
- 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数。
答:哈希切割、两个位图
3、布隆过滤器
- 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法。
答:
方法一:哈希切割(精确计算)
方法二:布隆过滤器(近似计算)
- 如何扩展BloomFilter使得它支持删除元素的操作。
答:把位图的每个单位都设计成一个小型计数器。