布隆过滤器是可以用于判断一个元素是不是在一个集合里,并且相比于其它的数据结构,布隆过滤器在空间和时间方面都有巨大的优势。布隆过滤器存储空间和插入/查询时间都是常数。但是它也是拥有一定的缺点:布隆过滤器是有一定的误识别率以及删除困难的。
假设有一个长度为m的bit类型数组,即数组只由 0 和 1 组成
再假设有k个hash函数,这些函数的输出域s都大于等于m,并且这些hash函数彼此完全独立,那么对于同一个对象(电话号码)经过k个hash函数算出来也是完全独立,可能相同 也可能不同,然后对算出来的结果都对m取余,然后在bit array上把相应的位置设置为1 ,10000条电话号码都经过相应的处理,bitmap中已经有大部分的位置都设置为1 了,
但是值得注意的是,当所有位都是1后该布隆过滤器失效
那么在检查阶段如何检查一个电话号码是否加入过呢? 用p表示11位的电话号码,要检查是否被加入过,就把p通过k个hash函数计算出k个值,然后把k个值都取余(%m)
就可以得到[0,m-1] 范围上的k个值,接下来在bitmap中看这k个位置是否都为1,如果有一个不是1 ,那么说明p不在这个集合中,但是也可能存在误判的情况,也就是 bit中的k个位置已经是1了,但p并没有加入过集合。
如果bitmap的大小m相比输入的数据的个数 n 过小,失误率会变大, 那么确定布隆过滤器的大小m和哈希函数的个数k 就是关键了
查阅资料 布隆过滤器的大小由以下公式决定
m = - n*lnp / (ln2)^2
n = 10000 , 失误率p 为 0.00001
计算出 m ~ 239852.6041666...
随机生成10000条电话号码,实现bloom filter 查找某个电话号码是否存在
在测试时发现 @Test 每次都会新建对象 ,单独运行每一个test 不能共享变量,
所以最后选择读入phones.txt文件
所以为了使误差p 在 0.01% 左右
哈希函数的个数为 k =ln2 * m/n ~ 16
seeds 可取 16的长度
代码:
package com.pys.bloomfilter;
import java.util.BitSet;
public class BloomFilter{
private int[] seeds;
private int size; //bitmap的大小
private static BitSet bitmap;
//失误率
private MisjudgementRate rate;
private int cnt = 0;
private Double haveUsedRate;
public BloomFilter(int dataCount){
this(MisjudgementRate.MIDDLE,dataCount,null);
}
/**
* 初始化布隆过滤器
* @param rate 失误率
* @param dataCount 数据规模 10000 条电话号码
* @param haveUsedRate 当布隆过滤器使用率达到 haveUsedRate 后就自动清空,重新使用,防止失效
*/
public BloomFilter(MisjudgementRate rate,int dataCount,Double haveUsedRate){
//每一个电话号码分配 rate.seeds.length 位
long bitSize = rate.seeds.length * dataCount; //bitmap大小
if(bitSize < 0|| bitSize >Integer.MAX_VALUE){ //数据过大
throw new RuntimeException("数据过大");
}
this.rate = rate;
seeds = this.rate.seeds;
size = (int) bitSize;
bitmap = new BitSet(size);
this.haveUsedRate = haveUsedRate;
}
//添加入集合
public void add(String data){
for(int i = 0;i<seeds.length;i++){
int index = hash(data,seeds[i]);
if(!bitmap.get(index)){
bitmap.set(index,true);//在位图中把该位置标记为1
this.cnt++;
}
}
}
public boolean check(String data){
for(int i = 0;i<seeds.length;i++){
int index = hash(data,seeds[i]);
if(!bitmap.get(index)){
//如果该位置不是1 说明这个号码不存在
return false;
}
}
//如果都是1说明存在
return true;
}
// //如果不存在就进行记录并返回,如果存在就返回true
// public boolean addIfNotExist(String data){
// int[] indexs = new int[seeds.length];
// boolean exists = true;
// int index;
//
// for(int i = 0 ;i<seeds.length ;i++){
// //记录索引
// indexs[i] = index = hash(data,seeds[i]);
// if(exists){
// if(!bitmap.get(index)){
// //如果在位图中该位置不是1 那么该号码第一次出现
// exists = false;
// bitmap.set(index,true);//将这个位置置1
// }
// }else{ //不存在 将当前的位置1
// bitmap.set(index,true);
// }
// }
// return exists;
// }
//判断该过滤器的使用率是否已经到达上限,是否要清空
public void checkNeedClear(){
if(haveUsedRate != null){
if(getUseRate() >= haveUsedRate){
this.bitmap.clear();//清空
this.cnt = 0;
}
}
}
public double getUseRate(){
return (double) cnt / (double) size;
}
//构造hash函数
public int hash(String data,int seed){
char[] chars = data.toCharArray();
int code = 0;
for(int i = 0;i<chars.length;i++){
code = i*code + chars[i];
}
code = code * seed % size;
return Math.abs(code); //防止溢出
}
public enum MisjudgementRate {
// 每一个字符串在位图中占几位 ,取质数可以降低误差率
VERY_SMALL(new int[]{2, 3, 5, 7}),
SMALL(new int[]{2, 3, 5, 7, 11, 13, 17, 19}),
//16
MIDDLE(new int[]{2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53});
private int[] seeds;
private MisjudgementRate(int[] seeds) {
this.seeds = seeds;
}
public int[] getSeeds() {
return seeds;
}
public void setSeeds(int[] seeds) {
this.seeds = seeds;
}
}
}
Test
package com.pys.bloomfilter;
import org.junit.Test;
import java.io.*;
import java.util.Map;
public class MainTest {
final int SIZE = 10000;
String[] phones = new String[SIZE];
BloomFilter bloomFilter = new BloomFilter(SIZE);
String filePath = "/Users/pingyunshangpingyunshang/Downloads/phones.txt";
@Test
public void init() {
//生成10000个随机号码 但是因为首位是1 所有只要后10位随机即可 也就是长度为10的字符
OutputStreamWriter writer = null;
try {
writer = new OutputStreamWriter(new FileOutputStream(filePath));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
for (int i = 0; i < SIZE; i++) {
String res = "1";
for (int j = 0; j < 10; j++) {
res += ((int) (Math.random() * 10)) + "";
}
phones[i] = res;
try {
writer.write(phones[i]);
writer.write("\n");
} catch (IOException e) {
e.printStackTrace();
}
}
try {
if (writer != null) {
writer.flush();
writer.close();
System.out.println("success");
}
} catch (IOException e) {
e.printStackTrace();
}
}
@Test
public void test() {
FileReader fileReader = null;
BufferedReader reader = null;
try {
//读文件
fileReader = new FileReader(filePath);
reader = new BufferedReader(fileReader);
String line;
while ((line = reader.readLine()) != null) {
bloomFilter.add(line);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//关闭资源
if (fileReader != null) {
try {
fileReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
System.out.println("14930625616是否存在:"+bloomFilter.check("14930625616"));
System.out.println("17079111577是否存在:"+bloomFilter.check("17079111577"));
System.out.println("10636734869是否存在:"+bloomFilter.check("10636734869"));
System.out.println("15058125173是否存在:"+bloomFilter.check("15058125173"));
System.out.println("17604271316是否存在:"+bloomFilter.check("17604271316"));
System.out.println("布隆过滤器使用率"+bloomFilter.getUseRate());
}
}
当变化seeds时 误判率不同