冷饭新炒:理解布隆过滤器算法的实现原理 (2)

这里可以推算一下表格中最大参数所需要的空间极限,假设n为10亿,m/n = 32,那么m为320亿,而k为24,此时的误判率为2.17e-07(0.000000217),需要空间3814.69727m。一般规律是:

当k固定的时候,m/n越大,误判率越小

当m/n固定的时候,k越大,误判率越大

通常情况下,k需要固定,而n是无法确定准确值,最好要评估增长趋势预先计算一个比较大的m值去降低误判率,当然也要权衡m值过大导致空间消耗过大的问题。

既然参数的关系式都已经有推导结果,可以基于关系式写一个参数生成器:

import java.math.BigDecimal; import java.math.RoundingMode; public class BloomFilterParamGenerator { public BigDecimal falsePositiveRate(int m, int n, int k) { double temp = Math.pow(1 - Math.exp(Math.floorDiv(-k * n, m)), k); return BigDecimal.valueOf(temp).setScale(10, RoundingMode.FLOOR); } public BigDecimal kForMinFalsePositiveRate(int m, int n) { BigDecimal k = BigDecimal.valueOf(Math.floorDiv(m, n) * Math.log(2)); return k.setScale(10, RoundingMode.FLOOR); } public BigDecimal bestM(int n, double falsePositiveRate) { double temp = log2(Math.exp(1) + Math.floor(1 / falsePositiveRate)); return BigDecimal.valueOf(n).multiply(BigDecimal.valueOf(temp)).setScale(10, RoundingMode.FLOOR); } public double log2(double x) { return Math.log(x) / Math.log(2); } public static void main(String[] args) { BloomFilterParamGenerator generator = new BloomFilterParamGenerator(); System.out.println(generator.falsePositiveRate(2, 1, 2)); // 0.3995764008 System.out.println(generator.kForMinFalsePositiveRate(32, 1)); // 22.1807097779 System.out.println(generator.bestM(1, 0.3995764009)); // 2.2382615950 } }

这里的计算没有考虑严格的进位和截断,所以和实际的结果可能有偏差,只提供一个参考的例子。

布隆过滤器的优势和劣势

布隆过滤器的优势:

布隆过滤器相对于其他数据结构在时空上有巨大优势,占用内存少,查询和插入元素的时间复杂度都是O(k)

可以准确判断元素不存在于布隆过滤器中的场景

散列函数可以独立设计

布隆过滤器不需要存储元素本身,适用于某些数据敏感和数据严格保密的场景

布隆过滤器的劣势:

不能准确判断元素必定存在于布隆过滤器中的场景,存在误判率,在k和m固定的情况下,添加的元素越多,误判率越高

没有存储全量的元素,对于一些准确查询或者准确统计的场景不适用

原生的布隆过滤器无法安全地删除元素

这里留一个很简单的问题给读者:为什么原生的布隆过滤器无法安全地删除元素?(可以翻看之前的False Positive介绍)

布隆过滤器算法实现

著名的Java工具类库Guava中自带了一个beta版本的布隆过滤器实现,这里参考其中的源码实现思路和上文中的算法描述进行一次布隆过滤器的实现。先考虑设计散列函数,简单一点的方式就是参考JavaBean的hashCode()方法的设计:

// 下面的方法来源于java.util.Arrays#hashCode public static int hashCode(Object a[]) { if (a == null) return 0; int result = 1; for (Object element : a) result = 31 * result + (element == null ? 0 : element.hashCode()); return result; }

上面方法的31可以作为一个输入的seed,每个散列函数设计一个独立的seed,并且这个seed值选用素数基于字符串中的每个char进行迭加就能实现计算出来的结果是相对独立的:

import java.util.Objects; public class HashFunction { /** * 布隆过滤器容量 */ private final int m; /** * 种子 */ private final int seed; public HashFunction(int m, int seed) { this.m = m; this.seed = seed; } public int hash(String element) { if (Objects.isNull(element)) { return 0; } int result = 1; int len = element.length(); for (int i = 0; i < len; i++) { result = seed * result + element.charAt(i); } // 这里确保计算出来的结果不会超过m return (m - 1) & result; } }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zgwwjd.html