Bloom Filter 原理
下面来分析下它的实现原理。
官方的说法是:它是一个保存了很长的二级制向量,同时结合 Hash 函数实现的。
听起来比较绕,但是通过一个图就比较容易理解了。
如图所示:
- 首先需要初始化一个二进制的数组,长度设为 L(图中为 8),同时初始值全为 0 。
- 当写入一个
A1=1000
的数据时,需要进行 H 次hash
函数的运算(这里为 2 次);与 HashMap 有点类似,通过算出的HashCode
与 L 取模后定位到 0、2 处,将该处的值设为 1。 -
A2=2000
也是同理计算后将4、7
位置设为 1。 - 当有一个
B1=1000
需要判断是否存在时,也是做两次 Hash 运算,定位到 0、2 处,此时他们的值都为 1 ,所以认为B1=1000
存在于集合中。 - 当有一个
B2=3000
时,也是同理。第一次 Hash 定位到index=4
时,数组中的值为 1,所以再进行第二次 Hash 运算,结果定位到index=5
的值为 0,所以认为B2=3000
不存在于集合中。
整个的写入、查询的流程就是这样,汇总起来就是:
对写入的数据做 H 次 hash 运算定位到数组中的位置,同时将数据改为 1 。当有数据查询时也是同样的方式定位到数组中。 一旦其中的有一位为 0 则认为数据肯定不存在于集合,否则数据可能存在于集合中。
所以布隆过滤有以下几个特点:
- 只要返回数据不存在,则肯定不存在。
- 返回数据存在,但只能是大概率存在。
- 同时不能清除其中的数据。
第一点应该都能理解,重点解释下 2、3 点。
为什么返回存在的数据却是可能存在呢,这其实也和 HashMap
类似。
在有限的数组长度中存放大量的数据,即便是再完美的 Hash 算法也会有冲突,所以有可能两个完全不同的 A、B
两个数据最后定位到的位置是一模一样的。
这时拿 B 进行查询时那自然就是误报了。
删除数据也是同理,当我把 B 的数据删除时,其实也相当于是把 A 的数据删掉了,这样也会造成后续的误报。
基于以上的 Hash
冲突的前提,所以 Bloom Filter
有一定的误报率,这个误报率和 Hash
算法的次数 H,以及数组长度 L 都是有关的。
自己实现一个布隆过滤
算法其实很简单不难理解,于是利用 Java
实现了一个简单的雏形。
public class BloomFilters {
/**
* 容器长度
*/
private int arraySize;
/**
* 容器
*/
private int[] array;
/**
* 初始化数组容器
*/
public BloomFilters(int arraySize) {
this.arraySize = arraySize;
array = new int[arraySize];
System.out.println("初始化数组容器长度: " + initSize);
}
/**
* 写入数据
* @param key
*/
public void add(String data) {
// 第一处写入
int hash1 = hash1(data);
array[hash1 % size] = 1;
// 第二处写入
int hash2 = hash2(data);
array[hash2 % size] = 1;
// 第三处写入
int hash3 = hash3(data);
array[hash3 % size] = 1;
}
/**
* 判断数据是否存在
* - 返回true时不一样就表示数据存在(类似hash冲突);
* - 返回false时数据就一定不存在;
* @param key
* @return
*/
public boolean check(String data) {
int hash1 = hash1(data);
if(array[hash1 % size] == 0){
return false;
}
int hash2 = hash2(data);
if(array[hash2 % size] == 0){
return false;
}
int hash3 = hash3(data);
if(array[hash3 % size] == 0){
return false;
}
return true;
}
// 三种不同算法↓↓↓
/**
* hash 算法1
*/
private int hashcode_1(String key) {
int hash = 0;
int i;
for (i = 0; i < key.length(); ++i) {
hash = 33 * hash + key.charAt(i);
}
return Math.abs(hash);
}
/**
* hash 算法2
*/
private int hashcode_2(String data) {
final int p = 16777619;
int hash = (int) 2166136261L;
for (int i = 0; i < data.length(); i++) {
hash = (hash ^ data.charAt(i)) * p;
}
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
return Math.abs(hash);
}
/**
* hash 算法3
*/
private int hashcode_3(String key) {
int hash, i;
for (hash = 0, i = 0; i < key.length(); ++i) {
hash += key.charAt(i);
hash += (hash << 10);
hash ^= (hash >> 6);
}
hash += (hash << 3);
hash ^= (hash >> 11);
hash += (hash << 15);
return Math.abs(hash);
}
}
- 首先初始化了一个 int 数组。
- 写入数据的时候进行三次
hash
运算,同时把对应的位置置为 1。 - 查询时同样的三次
hash
运算,取到对应的值,一旦值为 0 ,则认为数据不存在。
// 测试
public static void main(String[] args) {
// 1.初始化数组容器长度1千w, 注意初始化容器大小一定要比预计装入的数据大,为了减少算法hash冲突。
BloomFilters bloomFilters = new BloomFilters(10000000);
// 2.装入10w条测试数据
for (int i = 0; i < 100000; i++) {
bloomFilters.add(i+"");
}
bloomFilters.add("测试数据");
bloomFilters.add("测试数据A");
bloomFilters.add("测试数据B");
bloomFilters.add("测试数据C");
// 3.查询是否存在
System.out.println(bloomFilters.contain(255555+"")); // false
System.out.println(bloomFilters.contain("测试数据")); // true
System.out.println(bloomFilters.contain(5600+"")); // true
System.out.println(bloomFilters.contain("测试数据B")); // true
System.out.println(bloomFilters.contain("AAA")); // false
}