【恋上数据结构】布隆过滤器(Bloom Filter)原理及实现
布隆过滤器(Bloom Filter)
- 引出布隆过滤器(判断元素是否存在)
- 布隆过滤器介绍(概率型数据结构)
- 布隆过滤器的原理(二进制 + 哈希函数)
- 布隆过滤器的误判率(公式)
- 布隆过滤器的实现
- 布隆过滤器的构造
- 布隆过滤器 - 添加元素
- 设置指定位置元素的二进制值为1
- 布隆过滤器 - 判断元素是否存在
- 查看指定位置的二进制的值
- 布隆过滤器 - 完整代码
- 10亿网站爬虫问题
数据结构与算法笔记:恋上数据结构笔记目录
引出布隆过滤器(判断元素是否存在)
思考:如果要经常判断 1 个元素是否存在,要怎么做?
- 很容易想到使用哈希表(
HashSet
、HashMap
),将元素作为 key 去查找
时间复杂度:O(1),但是空间利用率不高,需要占用比较多的内存资源
如果需要编写一个网络爬虫去爬10亿个网站数据,为了避免爬到重复的网站,如何判断某个网站是否爬过?
- 很显然,
HashSet
、HashMap
并不是非常好的选择
是否存在时间复杂度低、占用内存较少的数据结构 ?
- 布隆过滤器(Bloom Filter)
布隆过滤器介绍(概率型数据结构)
1970年由布隆提出
- 它是一个空间效率高的概率型数据结构,可以用来告诉你:一个元素一定不存在或者可能存在。
优缺点:
- 优点:空间效率和查询时间都远远超过一般的算法
- 缺点:有一定的误判率、删除困难
布隆过滤器实质上是一个很长的二进制向量和一系列随机映射函数(Hash函数);
常见应用:
- 网页黑名单系统
- 垃圾邮件过滤系统
- 爬虫的网址判重系统
- 解决缓存穿透问题(后端开发)
布隆过滤器的原理(二进制 + 哈希函数)
假设布隆过滤器由 20位二进制、 3个哈希函数组成,每个元素经过哈希函数处理都能生成一个索引位置。
布隆过滤器的基础操作有两个:添加、查询
- 添加元素:将每一个哈希函数生成的索引位置都设为 1
- 查询元素是否存在:
如果有一个哈希函数生成的索引位置不为 1,就代表不存在(100%准确)
如果每一个哈希函数生成的索引位置都为 1,就代表存在(存在一定的误判率)
- 添加、查询的时间复杂度都是:O(k) ,k 是哈希函数的个数
- 空间复杂度是:O(m) ,m 是二进制位的个数
布隆过滤器的误判率(公式)
误判率 p 受 3 个因素影响:二进制位的个数 m、哈希函数的个数 k、数据规模 n。
误判率 p 的公式:
已知误判率 p、数据规模 n,求二进制位的个数 m、哈希函数的个数 k:
- 二进制位的个数 m:
- 哈希函数的个数 k:
布隆过滤器的实现
Guava: Google Core Libraries For Java(谷歌核心库中Java实现)
- https://mvnrepository.com/artifact/com.google.guava/guava
布隆过滤器的基础操作有两个:添加元素、查询元素是否存在
/*** 添加元素* @return true代表bit发送了变化*/
boolean put(T value);/*** 查询元素是否存在* @return false代表一定不存在, true代表可能存在*/
boolean contains(T value);
布隆过滤器的构造
根据上面的公式可知,布隆过滤器必然有2个全局变量:
bitSize
:二进制向量的长度(一共有多少个二进制位)hashSize
:哈希函数的个数
并且必然有个容器来存储这些二进制位:
bits
:这里选择long[]
来存储,因为1个long
可以表示64位bit
;(int[]
等数组也可以)
package com.mj;public class BloomFilter<T> {/*** 二进制向量的长度(一共有多少个二进制位)*/private int bitSize;/*** 二进制向量*/private long[] bits;/*** 哈希函数的个数*/private int hashSize;/*** 布隆过滤器的构造* @param n 数据规模* @param p 误判率, 取值范围(0, 1)*/public BloomFilter(int n, double p){if (n <= 0 || p <= 0 || p >= 1) { // 非法输入检测throw new IllegalArgumentException("wrong n or p");}// 根据公式求出对应的数据double ln2 = Math.log(2);// 求出二进制向量的长度bitSize = (int) (- (n * Math.log(p)) / (ln2 * ln2));hashSize = (int) (bitSize * ln2 / n);// bits数组的长度bits = new long[(bitSize + Long.SIZE - 1) / Long.SIZE]; // 分页公式// (64 + 64 - 1) / 64 = 127 / 64 = 1// (128 + 64 - 1) / 64 = 2// (130 + 64 - 1) / 64 = 3// 分页问题:// 每一页显示100条数据, pageSize = 100// 一共有999999条数据, n = 999999// 请问有多少页 pageCount = (n + pageSize - 1) / pageSize};}
测试一下,假设有1亿个数据,要求误判率为1%:
可以得到哈希函数的个数为 6,二进制位的个数是 958505837。
public static void main(String[] args) {BloomFilter<Integer> bf = new BloomFilter<>(1_0000_0000, 0.01);// 哈希函数的个数: 6// 二进制位的个数: 958505837
}
布隆过滤器 - 添加元素
设置指定位置元素的二进制值为1
比如要设置 100000
的 第2位bit 为 1,应当 100000 | 000100
,即 100000 | (1 << 2)
;
100000
| 000100 == (1 << 2)------------------100100
那么设置 value
的 第index位bit为 1,则是 value| (1 << index)
;
/*** 设置index位置的二进制为1*/
private boolean set(int index){// 对应的long值long value = bits[index / Long.SIZE];int bitValue = 1 << (index % Long.SIZE);bits[index / Long.SIZE] = value | bitValue;return (value & bitValue) == 0;
}
有了以上基础,可以实现布隆过滤器的添加元素操作:
/*** 添加元素*/
public boolean put(T value) {nullCheck(value);// 利用value生成 2 个整数int hash1 = value.hashCode();int hash2 = hash1 >>> 16;boolean result = false;for (int i = 1; i <= hashSize; i++) {int combinedHash = hash1 + (i * hash2);if (combinedHash < 0) {combinedHash = ~combinedHash;} // 生成一个二进制的索引int index = combinedHash % bitSize;// 设置第index位置的二进制为1if (set(index)) result = true;// 101010101010010101// | 000000000000000100 1 << index// 101010111010010101}return result;
}
布隆过滤器 - 判断元素是否存在
查看指定位置的二进制的值
比如要查看 10101111
的 第2位bit 为 1,应当 10101111 & 00000100
,即 10101111 & (1 << 2)
,只有指定位置的二进制的值为 0,返回值才会是 0,否则为 1;
10101111
& 00000100 == (1 << 2)--------------00000100 != 0, 说明index位的二进制为1
那么获取 value
的 第index位bit 的值,则是 value & (1 << index)
;
/*** 查看index位置的二进制的值* @param index* @return true代表1, false代表0*/
private boolean get(int index) {// 对应的long值long value = bits[index / Long.SIZE];return (value & (1 << (index % Long.SIZE))) != 0;
}
有了以上基础,可以实现布隆过滤器的判断一个元素是否存在操作:
/*** 判断一个元素是否存在*/
public boolean contains(T value) {nullCheck(value);// 利用value生成2个整数int hash1 = value.hashCode();int hash2 = hash1 >>> 16;for (int i = 1; i <= hashSize; i++) {int combinedHash = hash1 + (i * hash2);if (combinedHash < 0) {combinedHash = ~combinedHash;} // 生成一个二进制的索引int index = combinedHash % bitSize;// 查询第index位置的二进制是否为0if (!get(index)) return false;// 101010101010010101// | 000000000000000100 1 << index// 101010111010010101}return true;
}
布隆过滤器 - 完整代码
package com.mj;public class BloomFilter<T> {/*** 二进制向量的长度(一共有多少个二进制位)*/private int bitSize;/*** 二进制向量*/private long[] bits;/*** 哈希函数的个数*/private int hashSize;/*** 布隆过滤器的构造* @param n 数据规模* @param p 误判率, 取值范围(0, 1)*/public BloomFilter(int n, double p){if (n <= 0 || p <= 0 || p >= 1) { // 非法输入检测throw new IllegalArgumentException("wrong n or p");}// 根据公式求出对应的数据double ln2 = Math.log(2);// 求出二进制向量的长度bitSize = (int) (- (n * Math.log(p)) / (ln2 * ln2));hashSize = (int) (bitSize * ln2 / n);// bits数组的长度bits = new long[(bitSize + Long.SIZE - 1) / Long.SIZE]; // 分页公式// (64 + 64 - 1) / 64 = 127 / 64 = 1// (128 + 64 - 1) / 64 = 2// (130 + 64 - 1) / 64 = 3// 分页问题:// 每一页显示100条数据, pageSize = 100// 一共有999999条数据, n = 999999// 请问有多少页 pageCount = (n + pageSize - 1) / pageSize};/*** 添加元素*/public boolean put(T value) {nullCheck(value);// 利用value生成2个整数int hash1 = value.hashCode();int hash2 = hash1 >>> 16;boolean result = false;for (int i = 1; i <= hashSize; i++) {int combinedHash = hash1 + (i * hash2);if (combinedHash < 0) {combinedHash = ~combinedHash;} // 生成一个二进制的索引int index = combinedHash % bitSize;// 设置第index位置的二进制为1if (set(index)) result = true;// 101010101010010101// | 000000000000000100 1 << index// 101010111010010101}return result;}/*** 判断一个元素是否存在*/public boolean contains(T value) {nullCheck(value);// 利用value生成2个整数int hash1 = value.hashCode();int hash2 = hash1 >>> 16;for (int i = 1; i <= hashSize; i++) {int combinedHash = hash1 + (i * hash2);if (combinedHash < 0) {combinedHash = ~combinedHash;} // 生成一个二进制的索引int index = combinedHash % bitSize;// 查询第index位置的二进制是否为0if (!get(index)) return false;// 101010101010010101// | 000000000000000100 1 << index// 101010111010010101}return true;}/*** 设置index位置的二进制为1*/private boolean set(int index){// 对应的long值long value = bits[index / Long.SIZE];int bitValue = 1 << (index % Long.SIZE);bits[index / Long.SIZE] = value | bitValue;return (value & bitValue) == 0;/** 100000* | 000100 1 << 2* ---------* 100100*/}/*** 查看index位置的二进制的值* @param index* @return true代表1, false代表0*/private boolean get(int index) {// 对应的long值long value = bits[index / Long.SIZE];return (value & (1 << (index % Long.SIZE))) != 0;/** 10101111* & 00000100* -----------* 00000100 != 0, 说明index位的二进制为1*/}private void nullCheck(T value) {if (value == null) {throw new IllegalArgumentException("Value must not be null.");}}}
测试:
public static void main(String[] args) {BloomFilter<Integer> bf = new BloomFilter<>(1_00_0000, 0.01);for (int i = 1; i <= 1_00_0000; i++) {bf.put(i);}// for (int i = 1; i <= 1_00_0000; i++) {// System.out.println(bf.contains(i));
// }// 测试 1000000 条数据中的误判数int count = 0;for (int i = 1_00_0001; i <= 2_00_0000; i++) {if (bf.contains(i)){count++;}}System.out.println(count); // 20
}
10亿网站爬虫问题
回到一开始的问题:如果需要编写一个网络爬虫去爬10亿个网站数据,为了避免爬到重复的网站,如何判断某个网站是否爬过?
该问题的代码的大体框架如下:
// url数组
String[] urls = {};
BloomFilter<String> bf = new BloomFilter<>(10_0000_0000, 0.01);for (String url : urls) {if (bf.contains(url)) continue;// 爬这个url// ......// 爬完该url, 放进BloomFilter中bf.put(url);
}
根据布隆过滤器的原理:依靠哈希函数产生的索引,找到对应的二进制位值,为 1 则已经存在(存在误判),否则不存在(100%精确)。
bf.contains(url)
,如果已经爬过的网址在布隆过滤器中,必然会返回 true
,因此可以保证不会重复爬。但是有些网址可能没有爬过,但是经过哈希冲突,使得bf.contains(url)
返回也为 true
,可知有几率漏爬。
可以确保,这么写不会重复爬,但是有几率漏爬。
下面这种写法也可以:同样保证不重复爬,有几率漏爬。
String[] urls = {};
BloomFilter<String> bf = new BloomFilter<>(10_0000_0000, 0.01);for (String url : urls) {if (bf.put(url) == false) continue;// 爬这个url// ......
}
bf.put(url)
如果遇到已经在布隆过滤器中的元素,必然返回 false
,可以保证不重复爬。但是有些网址没有爬过,经过哈希冲突,使得 bf.put(url)
返回了 flase
,有几率漏爬。
【恋上数据结构】布隆过滤器(Bloom Filter)原理及实现相关推荐
- 布隆过滤器(Bloom Filter)原理及优缺点剖析
直观的说,bloom算法类似一个hash set,用来判断某个元素(key)是否在某个集合中. 和一般的hash set不同的是,这个算法无需存储key的值,对于每个key,只需要k个比特位,每个存储 ...
- mysql布隆过滤器源码_布隆过滤器(Bloom Filter)的原理和实现
什么情况下需要布隆过滤器? 先来看几个比较常见的例子 字处理软件中,需要检查一个英语单词是否拼写正确 在 FBI,一个嫌疑人的名字是否已经在嫌疑名单上 在网络爬虫里,一个网址是否被访问过 yahoo, ...
- js 数组 实现 完全树_Flink实例(六十八):布隆过滤器(Bloom Filter)的原理和实现 - 秋华...
什么情况下需要布隆过滤器? 先来看几个比较常见的例子 字处理软件中,需要检查一个英语单词是否拼写正确 在 FBI,一个嫌疑人的名字是否已经在嫌疑名单上 在网络爬虫里,一个网址是否被访问过 yahoo, ...
- Redis缓存穿透“新杀招“:布隆过滤器Bloom Filter
场景分析 这篇文章来讲述缓存穿透的补充解决方案. 为什么要用补充来形容呢? 在之前的文章中,我们提到缓存穿透的解决方案时,我是这么说的: 关于缓存穿透,我们可以在用户访问数据库后将null值存入Red ...
- 布隆过滤器速度_布隆过滤器(Bloom Filter)详解
布隆过滤器[1](Bloom Filter)是由布隆(Burton Howard Bloom)在1970年提出的.它实际上是由一个很长的二进制向量和一系列随机映射函数组成,布隆过滤器可以用于检索一个元 ...
- 布隆过滤器+布隆过滤器(Bloom Filter)详解
布隆过滤器+布隆过滤器(Bloom Filter)详解 程序 = 数据结构 + 算法 -- 图灵奖得主,计算机科学家N.Wirth(沃斯) A Bloom filter is a space effi ...
- HBase学习笔记(三)——布隆过滤器(Bloom Filter)的原理
文章目录 布隆过滤器介绍 布隆过滤器原理 布隆过滤器的优缺点与用途 布隆过滤器使用场景 布隆过滤器介绍 布隆过滤器(Bloom Filter)由 Burton Howard Bloom 在 1970 ...
- 布隆过滤器(Bloom Filter)的原理和实现
布隆过滤器使用场景 之前在<数学之美>里面看到过布隆过滤器的介绍.那么什么场景下面需要使用布隆过滤器呢? 看下下面几个问题 字处理软件中,需要检查一个英语单词是否拼写正确 在 FBI,一个 ...
- 布隆过滤器(Bloom Filter)详解——基于多hash的概率查找思想
转自:http://www.cnblogs.com/haippy/archive/2012/07/13/2590351.html 布隆过滤器[1](Bloom Filter)是由布隆(Burton ...
- 布隆过滤器Bloom Filter简介
背景: 如果在平时我们要判断一个元素是否在一个集合中,通常会采用查找比较的方法,下面分析不同的数据结构查找效率: 采用线性表存储,查找时间复杂度为O(N) 采用平衡二叉排序树(AVL.红黑树)存储,查 ...
最新文章
- idea搭建简单spring-boot项目
- Convolutional Neural Networks卷积神经网络
- 使用axis1.4生成webservice的客户端代码
- 如何自动检查内存泄漏和句柄耗尽
- 关于Angular使用http发送请求后的响应处理
- 云图说 | 分布式缓存服务DCS—站在开源Redis前辈的肩膀上,扬帆起航
- 录入成绩编程平均java_java 学习第二天小练习
- 分享一篇SCCM软件更新的故障排除
- linux 外接网卡驱动下载,绿联USB外置显卡+网卡驱动程序
- 解决远程桌面无法全屏的方法
- 两台局域网内的阿里云服务器传文件
- 炒外汇APP平台哪个好,排名前十的炒外汇平台
- 聊聊运营商对UDP的QoS限制和应对
- 【精度】概率论之概念解析:边缘化(Marginalisation)
- 用汉字能给计算机编程,为什么中国的程序员不能用中文来编程?
- Notepad 追加字符
- amos调节变量怎么画_结构方程模型建模思路及Amos操作--调节变量效果确定(一)(满满都是骚操作)...
- 如何修改Windows10系统文本背景色
- Symbol - 看似平凡的Symbol其实我们每天都在用 - 对象操作
- 超外差,固定码,破解