布隆过滤器(Bloom Filter)

  • 引出布隆过滤器(判断元素是否存在)
  • 布隆过滤器介绍(概率型数据结构)
  • 布隆过滤器的原理(二进制 + 哈希函数)
    • 布隆过滤器的误判率(公式)
  • 布隆过滤器的实现
    • 布隆过滤器的构造
    • 布隆过滤器 - 添加元素
      • 设置指定位置元素的二进制值为1
    • 布隆过滤器 - 判断元素是否存在
      • 查看指定位置的二进制的值
  • 布隆过滤器 - 完整代码
  • 10亿网站爬虫问题

数据结构与算法笔记:恋上数据结构笔记目录

引出布隆过滤器(判断元素是否存在)

思考:如果要经常判断 1 个元素是否存在,要怎么做?

  • 很容易想到使用哈希表HashSetHashMap),将元素作为 key 去查找
    时间复杂度:O(1),但是空间利用率不高,需要占用比较多的内存资源

如果需要编写一个网络爬虫去爬10亿个网站数据,为了避免爬到重复的网站,如何判断某个网站是否爬过?

  • 很显然,HashSetHashMap 并不是非常好的选择

是否存在时间复杂度低占用内存较少的数据结构 ?

  • 布隆过滤器(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)原理及实现相关推荐

  1. 布隆过滤器(Bloom Filter)原理及优缺点剖析

    直观的说,bloom算法类似一个hash set,用来判断某个元素(key)是否在某个集合中. 和一般的hash set不同的是,这个算法无需存储key的值,对于每个key,只需要k个比特位,每个存储 ...

  2. mysql布隆过滤器源码_布隆过滤器(Bloom Filter)的原理和实现

    什么情况下需要布隆过滤器? 先来看几个比较常见的例子 字处理软件中,需要检查一个英语单词是否拼写正确 在 FBI,一个嫌疑人的名字是否已经在嫌疑名单上 在网络爬虫里,一个网址是否被访问过 yahoo, ...

  3. js 数组 实现 完全树_Flink实例(六十八):布隆过滤器(Bloom Filter)的原理和实现 - 秋华...

    什么情况下需要布隆过滤器? 先来看几个比较常见的例子 字处理软件中,需要检查一个英语单词是否拼写正确 在 FBI,一个嫌疑人的名字是否已经在嫌疑名单上 在网络爬虫里,一个网址是否被访问过 yahoo, ...

  4. Redis缓存穿透“新杀招“:布隆过滤器Bloom Filter

    场景分析 这篇文章来讲述缓存穿透的补充解决方案. 为什么要用补充来形容呢? 在之前的文章中,我们提到缓存穿透的解决方案时,我是这么说的: 关于缓存穿透,我们可以在用户访问数据库后将null值存入Red ...

  5. 布隆过滤器速度_布隆过滤器(Bloom Filter)详解

    布隆过滤器[1](Bloom Filter)是由布隆(Burton Howard Bloom)在1970年提出的.它实际上是由一个很长的二进制向量和一系列随机映射函数组成,布隆过滤器可以用于检索一个元 ...

  6. 布隆过滤器+布隆过滤器(Bloom Filter)详解

    布隆过滤器+布隆过滤器(Bloom Filter)详解 程序 = 数据结构 + 算法 -- 图灵奖得主,计算机科学家N.Wirth(沃斯) A Bloom filter is a space effi ...

  7. HBase学习笔记(三)——布隆过滤器(Bloom Filter)的原理

    文章目录 布隆过滤器介绍 布隆过滤器原理 布隆过滤器的优缺点与用途 布隆过滤器使用场景 布隆过滤器介绍 布隆过滤器(Bloom Filter)由 Burton Howard Bloom 在 1970 ...

  8. 布隆过滤器(Bloom Filter)的原理和实现

    布隆过滤器使用场景 之前在<数学之美>里面看到过布隆过滤器的介绍.那么什么场景下面需要使用布隆过滤器呢? 看下下面几个问题 字处理软件中,需要检查一个英语单词是否拼写正确 在 FBI,一个 ...

  9. 布隆过滤器(Bloom Filter)详解——基于多hash的概率查找思想

    转自:http://www.cnblogs.com/haippy/archive/2012/07/13/2590351.html   布隆过滤器[1](Bloom Filter)是由布隆(Burton ...

  10. 布隆过滤器Bloom Filter简介

    背景: 如果在平时我们要判断一个元素是否在一个集合中,通常会采用查找比较的方法,下面分析不同的数据结构查找效率: 采用线性表存储,查找时间复杂度为O(N) 采用平衡二叉排序树(AVL.红黑树)存储,查 ...

最新文章

  1. idea搭建简单spring-boot项目
  2. Convolutional Neural Networks卷积神经网络
  3. 使用axis1.4生成webservice的客户端代码
  4. 如何自动检查内存泄漏和句柄耗尽
  5. 关于Angular使用http发送请求后的响应处理
  6. 云图说 | 分布式缓存服务DCS—站在开源Redis前辈的肩膀上,扬帆起航
  7. 录入成绩编程平均java_java 学习第二天小练习
  8. 分享一篇SCCM软件更新的故障排除
  9. linux 外接网卡驱动下载,绿联USB外置显卡+网卡驱动程序
  10. 解决远程桌面无法全屏的方法
  11. 两台局域网内的阿里云服务器传文件
  12. 炒外汇APP平台哪个好,排名前十的炒外汇平台
  13. 聊聊运营商对UDP的QoS限制和应对
  14. 【精度】概率论之概念解析:边缘化(Marginalisation)
  15. 用汉字能给计算机编程,为什么中国的程序员不能用中文来编程?
  16. Notepad 追加字符
  17. amos调节变量怎么画_结构方程模型建模思路及Amos操作--调节变量效果确定(一)(满满都是骚操作)...
  18. 如何修改Windows10系统文本背景色
  19. Symbol - 看似平凡的Symbol其实我们每天都在用 - 对象操作
  20. 超外差,固定码,破解

热门文章

  1. 穷人想变富,富人想变得更富
  2. 为什么要使用namedtuple?
  3. 手机快充功能到底是充电头的功劳还是线的功劳?
  4. @Scheduled cron表达式详解
  5. bison、lex版本不同造成的问题
  6. 安装netca_安装oracle 10g rac上的那些拦路虎
  7. avue-crud 使用_创建和使用CRUD存储过程
  8. pls-toolbox_使用T-SQL Toolbox数据库解决时区,GMT和UTC问题
  9. jpa 查询编写sql_学习编写基本SQL查询
  10. 德鲁伊 oltp oltp_深入研究内存中OLTP表的哈希索引