【数据结构】初入数据结构之布隆过滤器(Bloom Filter)及实现
布隆过滤器(Bloom Filter)
如果觉得对你有帮助,能否点个赞或关个注,以示鼓励笔者呢?!博客目录 | 先点这里
- 前提概念
- 什么是布隆过滤器?
- 布隆过滤器的作用?
- 布隆过滤器的缺陷
- 数据结构
- 关键因子
- 结构设计
- 代码实现
- 结构体
- 因子计算
- 哈希函数
- 插入元素
- 是否存在
- 问题
- 布隆过滤器可以节省多少空间?
前提概念
什么是布隆过滤器?
布隆过滤器(英语:Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难
- 布隆过滤器 -@维基百科
布隆过滤器的作用?
布隆过滤器有什么作用呢?通俗的讲,布隆过滤器的主要作用就是 "以更低的空间效率检索一个元素是否存在其中"
所以我们知道布隆过滤器的好处就是省空间,作用就是可以判断一个元素是否存在,那么结合实际的应用场景,通常布隆过滤器有哪些应用场景呢?
推荐去重过滤
- 比如对一个短视频推荐系统而言,在视频 feed 流场景,需求是向用户推荐了指定内容后,一段时间内不再重复出现已推荐过的内容,那么布隆过滤器就可以大派用场。
缓存穿透过滤
- 比如在互联网大并发场景下,为防止流量大面积穿透压垮 RDS,很多系统都会结合使用缓存去抗流量。所以我们也可以考虑使用布隆过滤器,针对识别出的外部非法请求,或是空结果请求,通通记录在过滤器中,并在下次访问中,提前由过滤器先行过滤
黑名单系统
- 比如某些系统需要记录百万,千万甚至更多的黑名单字符,使用传统的哈希表实现,需要使用到较大的存储空间,那么我们也可以采用布隆过滤器去提供相同的能力,并有效降低空间存储成本
布隆过滤器的弊端
我们都知道布隆过滤器相比传统 “检索元素是否存在” 的容器而言,具有节省空间存储成本的优势,在大数据领域则尤其明显。那么它就只有长处,毫无缺陷吗?
非也,布隆过滤器还有具有两个非常重要的缺陷
具有一定概率的误判率
,即假阳性率 fpp不具备删除元素的能力
具有一定的误判率是什么意思呢?意思就是可能会将 “不存在的元素误判为存在” ,而这个概率就是 fpp , 如何规避和解决?
- 首先必然存在误判,如无法接受误判,则布隆过滤器不适合使用。但可以根据业务需求降低概率,或是选择我们可接受的误判概率;
- 误判率 (假阳性率) 大小会受到哈希函数的个数,位数组的大小,以及预期元素个数等因素的影响
- 通常根据业务需求,选择可接受的 fpp 构建布隆过滤器,比如 fpp = 0.001, 每 1000 次误判 1 次
另外不具备删除元素的能力又是什么意思呢?根据布隆过滤器的设计,一个元素会被多个哈希函数计算,并得到多个不同的位置;因为是基于哈希实现,那么必然存在哈希冲突,即多个元素的某些哈希函数的哈希值是可能一致,即多个元素会共用一些位置。如果我们要删除某个元素,那么就需要将其所占有的位置重置为 0,但这可能会影响到其它共有该位置的元素的判断,所以综上布隆过滤器是不支持删除的。如果要删除,要怎么解决呢?
布隆过滤器无法解决,但是可以基于布隆过滤器进行扩展,衍生出可删除的过滤器, 比如 CBF (counting bloom filter)
、Cuckoo Filter (布谷鸟过滤器)
等, 如果要删除,则布隆过滤器的基本构型是不满足需求的,可以采用其他类似的数据结构
数据结构
关键因子
- bloom filter calculate
n
是过滤器预期支持的元素个数m
是过滤器位数组的大小,即该过滤器总共占用多少 bit 的空间c
是每个元素平均占用的空间p
是假阳性概率,即 fpp (false positive probability)k
哈希函数的个数
计算公式
- m=−nlog(p)1log(2)2m = -nlog(p)\frac{1}{log(2)^2}m=−nlog(p)log(2)21
- c=mnc = \frac{m}{n}c=nm
- p=(1−e−knm)k=(1−e−kmn)k=(1−e−kc)kp = (1-e^{\frac{-kn}{m}})^k = (1-e^{\frac{-k}{\frac{m}{n}}})^k = (1 - e^{\frac{-k}{c}})^kp=(1−em−kn)k=(1−enm−k)k=(1−ec−k)k
- k=mn∗ln2=0.7mn=0.7ck = \frac{m}{n} *ln2 = 0.7\frac{m}{n} = 0.7ck=nm∗ln2=0.7nm=0.7c
通常创建一个布隆过滤器,会需要用户提供想支持的 n
和预期的 p
, 然后通过公式得到其余最佳的参数
结构设计
布隆过滤器的数据结构其实也挺简单,如果之前有了解过位图的话,那么就更简单了。整个布隆过滤器的底层实现就是基于 “位数组 + 哈希函数” 实现的
假设某个布隆过滤器的位数组大小为 1000 bits, 即 1000 个位。由 3 个 hash functions 组成
当我们往该布隆过滤器传入一个元素 (Hello World) ,那么该元素就会经过 3 个 hash functions 计算,得到 3 个 hash 值,最后哈希值经过模运算映射到位数组的具体第 x 位上,并标记为 1
当我们要判断某个元素是否在布隆过滤器时,同理,也是将该元素传入,经过哈希函数计算,并得到映射位数组的索引值,判断这个几个索引的位是否为 1
, 如果都为 1
则代表该元素已经存在了,但只要有一个位置不为 1
,我们就认为该元素并不存在
代码实现
结构体
布隆过滤器结构主要可以由三部分构成,关键因子 (n,m,p,k…),哈希函数,位数组,所以假设我们要实现一个布隆过滤器,首先就要将以上三部分内容构建好,这里写一份伪代码,基本可以简单的体现出布隆过滤器的结构设计
public abstract class BloomFilter {// 字节数组byte[] bytes;// 关键因子int n;double p;int m;int k;// 哈希函数List<Hash> hashes;// 插入元素public void put(byte[] bs);// 是否存在public boolean contains(byte[] bs);
}
- 为什么 n, m 是
int
, 而不是long
, 因为 Java 中没有位数组的概念,所以我们就使用字节数组去实现位数组,而数组大小的最大值是 Integer.MAX_VALUE, 所以 n,m 也采用 int - 其实也并非没有位数组的概念,可以通过 Java 自带的 BitSet 去实现,会更简单,但是这里的目的是为了学习,所以就不直接使用轮子了。除此之外,还要很多实践中,会使用 long 数组去实现
因子计算
- 通过 (n,p) 计算最佳的 m
/*** Computes m (total bits of Bloom filter)*/public static int optimalNumOfBits(int n, double p) {if (p == 0) {p = Double.MIN_VALUE;}return (int) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));}
- 通过 (n,m) 计算最佳的 k
/*** Computes the optimal k */public static int optimalNumOfHashFunctions(int n, int m) {// (m / n) * log(2), but avoid truncation due to division!return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));}
- 通过 (n,m,k) 计算最佳的 p
/*** Computes the false positive probability* 1. p = (1 - e ^ (-kn/m))^k*/public static double optimalFpp(int n, int m, int k) {// keep 5 digits after the decimal pointint point = 100000;return (double) Math.round((point * Math.pow(1 - Math.exp((double) (-k * n) / m), k))) / point;}
- 通过 (n,m) 计算 c
/*** Bits occupied each element*/public static double bitsOfElement(int n, int m) {return (double) m / n;}
- 通常我们可以通过用户传递的 n 和 p,构造出一个预期的布隆过滤器
哈希函数
哈希函数可以采用各种混合的哈希算法去搭配,我这件为了简单实现,建议采用 murmur3 hash, 通过加不同的 seed 盐去实现不同的哈希效果
// 成员变量 k 个哈希函数private List<Hash> hashes;// 获得 k 个索引值public int[] hashes(byte[] bs) {int[] indexs = new int[hashes.size()];for (int i = 0; i < hashes.size(); i++) {// int index = (int) ((hashes.get(i).hashToLong(bs) & Long.MAX_VALUE) % this.m);indexs[i] = index;}return indexs;}// 哈希接口public interface Hash {long hashToLong(byte[] bs);}
这里是伪代码,Hash 接口没有实现,可以采用 murmur3 加盐实现多个哈希函数
- hashToLong 就是传入数据,获得一个 64 位的哈希值
- 为什么要再
& Long.MAX_VALUE
呢?目的是为了让 hash 值是一个正数,(与01111...
相与,自然就是正数啦)
- 为什么要再
- 为什么要
% m
, 就是布隆过滤器的总的位数, 通过 hash 值求得在过滤器中的位置
插入元素
// 插入元素&是否存在public synchronized void put(byte[] bs) {int[] indexs = hashes(bs);for (int index : indexs) {// 所在的具体字节byte bits = (byte) (index / B);// 所在的具体位byte offset = (byte) (1 << (B_MASK - (index % B)));bytes[index / B] = (byte) (bits | offset);}}
B = 8
, 代表一个字节 8 位; B_MASK 是掩码,B_MASK = B - 1
,为了做取模运算- 首先得到 index, 通过
index / B
得到 index 所在那个 byte - 其次通过
index % B
得到所在该 byte 的具体位索引,比如等于 2, (00100000
, 一个 byte 8 位,1
所在的位置就是 2) - 通过
index % B
知道了具体的位索引后,为了将该位置变成 1,那么我们就需要得到位加法的偏移量,即得到00100000
去做加法 - 怎么得到那?即把
00000001
左移B_MASK - (index % B)
= 7 - 2 = 5 位,左移 5 位后,得到00100000
- 假设所在字节为
10000001
, 我们要将索引为 2 的位置变为 1, 则两个 byte 做或运算,即位加法即可10000001
|00100000
=10100001
- 最后覆盖 index 所在的位即可
是否存在
// 是否存在public synchronized boolean contains(byte[] bs) {int[] indexs = hashes(bs);for (int index : indexs) {byte bits = (byte) (index / B);byte offset = (byte) (1 << (B_MASK - (index % B)));if ((bits & offset) == 0) {return false;}}return true;}
- 是否存在的位运算原理跟插入元素是一致的,直接看插入即可
- 需要区分的是因为有多个哈希函数,自然就有多个 index, 只要有一个 index 不为 1,我们就认为其不存在
伪代码框架
要实现一个完整的布隆过滤器其实也不难,我这里附上一份简化的布隆过滤器伪代码,基本上除了哈希函数的选型留空,其余逻辑都有基本的实现,可以简易的参考,配合理解学习
/*** @author SnailMann*/
public abstract class BloomFilter {// A byte has 8 bitsprivate static final int B = 8;// B maskprivate static final int B_MASK = B - 1;// 字节数组private byte[] bytes;// 关键因子private int n;private int p;private int m;private int k;List<Hash> hashes;public BloomFilter(int n, int p) {this.n = n;this.p = p;this.m = optimalNumOfBits(n, p);this.k = optimalNumOfHashFunctions(this.n, this.m);this.bytes = new byte[this.m];}// 插入元素&是否存在public synchronized void put(byte[] bs) {int[] indexs = hashes(bs);for (int index : indexs) {// 所在的具体字节byte bits = (byte) (index / B);// 所在的具体位byte offset = (byte) (1 << (B_MASK - (index % B)));bytes[index / B] = (byte) (bits | offset);}}public synchronized boolean contains(byte[] bs) {int[] indexs = hashes(bs);for (int index : indexs) {byte bits = (byte) (index / B);byte offset = (byte) (1 << (B_MASK - (index % B)));if ((bits & offset) == 0) {return false;}}return true;}public int[] hashes(byte[] bs) {int[] indexs = new int[hashes.size()];for (int i = 0; i < hashes.size(); i++) {int index = (int) ((hashes.get(i).hashToLong(bs) & Long.MAX_VALUE) % this.m);indexs[i] = index;}return indexs;}private static int optimalNumOfHashFunctions(int n, int m) {return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));}private static int optimalNumOfBits(int n, double p) {if (p == 0) {p = Double.MIN_VALUE;}return (int) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));}public interface Hash {long hashToLong(byte[] bs);}}
如果需要完整可用的实现,可以参考博主之前实现比较完善的代码
- 支持 LRU 的布隆过滤器 - @ SnailMann
问题
布隆过滤器可以节省多少空间?
我们都知道布隆过滤器可以大大节省空间,那么它到底可以节省多少的空间呢?**这要如何去计算呢?**在我们学习了关键因子的计算后,这简直就是一件小事情
我们知道 c = m/n
,c
代表每个元素在布隆过滤器中平均所占用的空间,即多少的 bits。我们假设构建一个满足 p = 0.001, n = 10000
条件的 Filter
经过计算可以知道 m = 143776, k = 10
, 那么平均每个元素所占用的空间就是 c = m/n = 14.38 bits
。通常主键的类型会是是 32/64 整型或是字符串
- 假设我们要判断的元素是 64 位的整型,一个 64 位的数值占 64 bits, 所以每个元素我们就压缩了近 (64-14.38/64) = 63% 的空间
- 假设我们要判断的元素是 32 长度的字符串,而每个字符串就占用 32 * 8 = 256 bits, 那么每个元素我们就将近压缩了 94% 的空间
这是单个元素空间压缩的计算方式,如果觉得不够直观,那我们可以按照总量来计算, 假设 n = 100000000 (1 亿), p = 0.001, 保持 c = 14.38 ,那么 m 就会是1437758757 bits (171.39MB)。假设依然是 32 长度的字符串,那么要存储 1 亿个这样的字符,我们就需要 256 * 100000000 = 25,600,000,000 = 2.98 GB
的空间大小, 而如果我们采用布隆过滤器,则只需要 171 MB
如果你觉得这空间压缩也不过如此,那么就将数据量再放大点,加个 10000 倍,是不是就可以闻到
【数据结构】初入数据结构之布隆过滤器(Bloom Filter)及实现相关推荐
- mysql布隆过滤器源码_布隆过滤器(Bloom Filter)的原理和实现
什么情况下需要布隆过滤器? 先来看几个比较常见的例子 字处理软件中,需要检查一个英语单词是否拼写正确 在 FBI,一个嫌疑人的名字是否已经在嫌疑名单上 在网络爬虫里,一个网址是否被访问过 yahoo, ...
- 布隆过滤器+布隆过滤器(Bloom Filter)详解
布隆过滤器+布隆过滤器(Bloom Filter)详解 程序 = 数据结构 + 算法 -- 图灵奖得主,计算机科学家N.Wirth(沃斯) A Bloom filter is a space effi ...
- js 数组 实现 完全树_Flink实例(六十八):布隆过滤器(Bloom Filter)的原理和实现 - 秋华...
什么情况下需要布隆过滤器? 先来看几个比较常见的例子 字处理软件中,需要检查一个英语单词是否拼写正确 在 FBI,一个嫌疑人的名字是否已经在嫌疑名单上 在网络爬虫里,一个网址是否被访问过 yahoo, ...
- 布隆过滤器速度_布隆过滤器(Bloom Filter)详解
布隆过滤器[1](Bloom Filter)是由布隆(Burton Howard Bloom)在1970年提出的.它实际上是由一个很长的二进制向量和一系列随机映射函数组成,布隆过滤器可以用于检索一个元 ...
- Redis缓存穿透“新杀招“:布隆过滤器Bloom Filter
场景分析 这篇文章来讲述缓存穿透的补充解决方案. 为什么要用补充来形容呢? 在之前的文章中,我们提到缓存穿透的解决方案时,我是这么说的: 关于缓存穿透,我们可以在用户访问数据库后将null值存入Red ...
- 布隆过滤器 Bloom Filter
目录 一 前言 二 布隆过滤器 三 Redis实现 四 Guava实现 五 扩展知识点 一 前言 假如有一个15亿用户的系统,每天有几亿用户访问系统,要如何快速判断是否为系统中的用户呢? 方法一,将1 ...
- 布隆过滤器(Bloom Filter)详解——基于多hash的概率查找思想
转自:http://www.cnblogs.com/haippy/archive/2012/07/13/2590351.html 布隆过滤器[1](Bloom Filter)是由布隆(Burton ...
- 布隆过滤器(Bloom Filter)原理及优缺点剖析
直观的说,bloom算法类似一个hash set,用来判断某个元素(key)是否在某个集合中. 和一般的hash set不同的是,这个算法无需存储key的值,对于每个key,只需要k个比特位,每个存储 ...
- 布隆过滤器Bloom Filter简介
背景: 如果在平时我们要判断一个元素是否在一个集合中,通常会采用查找比较的方法,下面分析不同的数据结构查找效率: 采用线性表存储,查找时间复杂度为O(N) 采用平衡二叉排序树(AVL.红黑树)存储,查 ...
- 布隆过滤器(Bloom Filter)算法
布隆过滤器原理 开发一个电商项目,因为数据量一直在增加(已达亿级),所以需要重构之前开发好的秒杀功能,为了更好的支持高并发,在验证用户是否重复购买的环节,就考虑用布隆过滤器. 也顺便更加深入的去了解下 ...
最新文章
- MSSQL扫盲系列(4)-系统函数
- 列举一些RNN类模型的常见使用形式以及常见的应用
- php 删除子字符串函数,PHP删除字符串中的任何字符函数
- 一次单核CPU占用过高问题的处理
- 大棚骨架搭建好 科学施肥增收增产
- 汇编语言位向量(位映射)
- mysql 测试快生产慢_生产上MySQL慢查询优化实战,SQL优化实战
- GNU make manual 翻译(七十三)
- CentOS6.4 Install FTP
- React开发(210):react中try...catch..
- 为何只能在其关联的线程内启动timer?
- java 顺序表的实现_顺序表的简单实现(Java)
- alexa工具条下载_如何聆听(和删除)您给Alexa的每条命令
- (十一)【数电】(组合逻辑电路)数据分配器和数据选择器
- NoSuchMethodError: No virtual method(Ljava/io/File;)V in class Landroid/media/MediaRec
- 利用继电器实现防抖自锁功能
- a1708硬盘转接口_macbook pro2017 a1708转接卡更换大容量硬盘,Apple/苹果笔记本电脑_好文分享_优购 - 荐优,购适...
- 10个值得珍藏的4K高清壁纸网站推荐
- 习题6-8 单词首字母大写 (15 分)
- 如何通过手机APP远程控制PLC