18.布隆过滤器的实现及应用

布隆过滤器介绍

布隆过滤器(Bloom Filter)由Burton Howard Bloom在1970年提出,它实际上是一个很长的二进制向量(位数组) 和一系列 随机映射函数(哈希)。布隆过滤器可以用于检索一个元素是否在一个集合中,是一种空间查询率高的概率型数据结构。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。

它专门用来检测集合中是否存在特定的元素。在这里听起来很平常的需求,所以为什么要使用这种数据结构?

什么情况下需要用到布隆过滤器?

我们先来看几个比较常见的例子:

  • 在字处理软件中,需要检查一个英语单词是否拼写正确;
  • 网页爬虫对URL去重,避免爬取相同的URL地址;
  • 反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否是垃圾邮箱;
  • 缓存穿透,将已经存在的缓存放在布隆中,当黑客访问不存在的缓存时迅速返回避免缓存及DB挂掉。

这几个例子有一个共同的特点:如何判断一个元素是否存在一个集合中?

常规思路:

  • 数组
  • 链表
  • 树、平衡二叉树、Trie(字典树)
  • Map(红黑树)
  • 哈希表

虽然上面描述的这几种数据结构配合常见的排序、二分搜索可以快速高效的处理绝大部分判断元素是否存在集合中的需求。但是当集合里面的元素数量足够大,如果有500W条记录甚至1亿条记录呢?这个时候常规的数据结构的问题就凸显出来了。数组、链表、树等数据结构回存储元素的内容,一旦数据量过大,消耗的内存约会呈现线性增长,最终达到瓶颈。

有的同学可能回问,哈希表不是效率很高吗?查询效率可以达到O(1)。但是哈希表需要消耗的内存依然很高。使用哈希表存储1亿个垃圾email地址的消耗?哈希表的做法:首先,哈希函数将一个email地址映射成8字节信息指纹;考虑到哈希表存储效率通常小于50%(哈希冲突);因此消耗的内存:8 * 2 * 1亿 字节 = 1.6G内存,普查计算机是无法提供如此大的内存。这个时候,布隆过滤器(Bloom Filter)就应运而生。

在理解布隆过滤器的原理时,先来回顾一下哈希函数的知识。

哈希函数

哈希函数的概念是:将任意大小的数据转换成特定大小的数据的函数,转换后的数据称为哈希值或哈希编码。

对于哈希表来说它不仅有哈希函数来得到这么一个index值,且它会把整个要存的元素全部都放到哈希表里面取,这是一个没有误差的数据结构且有多少的元素,每个元素有多大,那么所有的这些元素需要占的内存空间在哈希表里面都要找相应的内存的大小给存进来。

那么在很多时候我们在工业级应用的时候,发现我们并不需要存所有元素本身,而只需要存一个信息,就是说这个元素在我这个表里面到底是有还是没有。在这种情况下如果只要查询有还是没有的时候,这个时候我们就需要一种更高效的数据结构,更高效的数据结构可以导致的一个结果,就这里有很多元素要存的话,但是我们这个表的话所需要的内存空间很少,同时的话我们不需要把元素本身全部存起来,我们只需要说把这个东西到底是有还是没有。接下来我们看这个数据结构到底是怎么设计出来的。

布隆过滤器原理

布隆过滤器(Bloom Filter)的核心实现是一个超大的位数组几个哈希函数。假设位数组的长度为 m,哈希函数的个数为k

具体的操作流程:假设集合里面有3个元素 {x,y,z}, 哈希函数的个数为3。

  • 首先将位数组进行初始化,将里面每个位都设置为0。
  • 对于集合里面的每个元素,将元素依次通过3个哈希函数进行映射,每次映射都会产生一个哈希值,这个值对应位数组上面的一个点,然后将位数组对应的位置标记位1。
  • 查询 W 元素是否存在集合中的时候,同样的方法将W通过哈希表映射到位数组上的3个点。如果3个点的其中有一个点不为1,则可以判断该元素一定不存在集合中。反之,如果3个点都为1,则该元素可能存在集合中。

注意:此处不能判断该元素是否一定存在集合中,可能存在一定的误判率。可以从图中看到:假设某个元素通过映射对应下标为4,5,6这3个点。虽然这3个点都为1,但是很明显这3个点是不同元素经过哈希表得到的位置,因此这种情况说明元素虽然不在集合中,也可能对应的都是1,这就是误判率存在的原因。

布隆过滤器的设计

布隆过滤器思路比较简单,但是对于布隆过滤器的随机映射函数设计,需要计算几次,向量长度设置多少比较合适,这个才是需要认真讨论的。

  • 如果向量长度太短,会导致误判率直线上升。
  • 如果向量太长,会浪费大量内存。
  • 如果计算次数过多,会占用计算资源,且很容易很快就把过滤器填满。

布隆过滤器的简单实现

布隆过滤器添加元素

  • 将要添加的元素给k个哈希函数
  • 得到对应于位数组上的k个位置
  • 将这k个位置设为1

布隆过滤器查询元素

  • 将要查询的元素给k个哈希函数
  • 得到对应于位数组上的k个位置
  • 如果k个位置有一个为0,则肯定不在集合中
  • 如果k个位置全部为1,则可能在集合中

java实现

//Java
public class BloomFilter {private static final int DEFAULT_SIZE = 2 << 24;private static final int[] seeds = new int[] { 5, 7, 11, 13, 31, 37, 61 };private BitSet bits = new BitSet(DEFAULT_SIZE);private SimpleHash[] func = new SimpleHash[seeds.length];public BloomFilter() {for (int i = 0; i < seeds.length; i++) {func[i] = new SimpleHash(DEFAULT_SIZE, seeds[i]);}}public void add(String value) {for (SimpleHash f : func) {bits.set(f.hash(value), true);}}public boolean contains(String value) {if (value == null) {return false;}boolean ret = true;for (SimpleHash f : func) {ret = ret && bits.get(f.hash(value));}return ret;}// 内部类,simpleHashpublic static class SimpleHash {private int cap;private int seed;public SimpleHash(int cap, int seed) {this.cap = cap;this.seed = seed;}public int hash(String value) {int result = 0;int len = value.length();for (int i = 0; i < len; i++) {result = seed * result + value.charAt(i);}return (cap - 1) & result;}}
}

Python实现

# Python
from bitarray import bitarray
import mmh3
class BloomFilter: def __init__(self, size, hash_num): self.size = size self.hash_num = hash_num self.bit_array = bitarray(size) self.bit_array.setall(0) def add(self, s): for seed in range(self.hash_num): result = mmh3.hash(s, seed) % self.size self.bit_array[result] = 1 def lookup(self, s): for seed in range(self.hash_num): result = mmh3.hash(s, seed) % self.size if self.bit_array[result] == 0: return "Nope" return "Probably"
bf = BloomFilter(500000, 7)
bf.add("dantezhao")
print (bf.lookup("dantezhao"))
print (bf.lookup("yyj"))

C/C++实现

C/C++#include <iostream>
#include <bitset>
#include <cmath>using namespace std;typedef unsigned int uint;
const int DEFAULT_SIZE = 1 << 20;
const int seed[] = { 5, 7, 11, 13, 31, 37, 61 };class BloomFilter {
public:BloomFilter() : hash_func_count(3) {}BloomFilter(int bitsize, int str_count) { hash_func_count = ceil((bitsize / str_count) * log(2));}~BloomFilter() {}uint RSHash(const char *str, int seed);void Add(const char *str);bool LookUp(const char *str);private:int hash_func_count;bitset<DEFAULT_SIZE> bits;
};uint BloomFilter::RSHash(const char *str, int seed) {  uint base = 63689;uint hash = 0;    while (*str) {    hash = hash * base + (*str++);    base *= seed;    }    return (hash & 0x7FFFFFFF);
}    void BloomFilter::Add(const char* str) {int index = 0;for(int i = 0; i < hash_func_count; ++i) {index = static_cast<int>(RSHash(str, seed[i])) % DEFAULT_SIZE;bits[index] = 1;}return ;
}bool BloomFilter::LookUp(const char* str) {int index = 0;for(int i = 0; i < hash_func_count; ++i) {index = static_cast<int>(RSHash(str, seed[i])) % DEFAULT_SIZE;if (!bits[index]) return false; }return true;
}

javascript实现

// JavaScript
class BloomFilter {constructor(maxKeys, errorRate) {this.bitMap = [];this.maxKeys = maxKeys;this.errorRate = errorRate;// 位图变量的长度,需要根据maxKeys和errorRate来计算this.bitSize = Math.ceil(maxKeys * (-Math.log(errorRate) / (Math.log(2) * Math.log(2))));// 哈希数量this.hashCount = Math.ceil(Math.log(2) * (this.bitSize / maxKeys));// 已加入元素数量this.keyCount = 0;}bitSet(bit) {let numArr = Math.floor(bit / 31);let numBit = Math.floor(bit % 31);this.bitMap[numArr] |= 1 << numBit;}bitGet(bit) {let numArr = Math.floor(bit / 31);let numBit = Math.floor(bit % 31);return (this.bitMap[numArr] &= 1 << numBit);}add(key) {if (this.contain(key)) {return -1;}let hash1 = MurmurHash(key, 0, 0),hash2 = MurmurHash(key, 0, hash1);for (let i = 0; i < this.hashCount; i++) {this.bitSet(Math.abs(Math.floor((hash1 + i * hash2) % this.bitSize)));}this.keyCount++;}contain(key) {let hash1 = MurmurHash(key, 0, 0);let hash2 = MurmurHash(key, 0, hash1);for (let i = 0; i < this.hashCount; i++) {if (!this.bitGet(Math.abs(Math.floor((hash1 + i * hash2) % this.bitSize)))) {return false;}}return true;}
}/*** MurmurHash** 参考 http://murmurhash.googlepages.com/** data:待哈希的值* offset:* seed:种子集**/
function MurmurHash(data, offset, seed) {let len = data.length,m = 0x5bd1e995,r = 24,h = seed ^ len,len_4 = len >> 2;for (let i = 0; i < len_4; i++) {let i_4 = (i << 2) + offset,k = data[i_4 + 3];k = k << 8;k = k | (data[i_4 + 2] & 0xff);k = k << 8;k = k | (data[i_4 + 1] & 0xff);k = k << 8;k = k | (data[i_4 + 0] & 0xff);k *= m;k ^= k >>> r;k *= m;h *= m;h ^= k;}// avoid calculating modulolet len_m = len_4 << 2,left = len - len_m,i_m = len_m + offset;if (left != 0) {if (left >= 3) {h ^= data[i_m + 2] << 16;}if (left >= 2) {h ^= data[i_m + 1] << 8;}if (left >= 1) {h ^= data[i_m];}h *= m;}h ^= h >>> 13;h *= m;h ^= h >>> 15;return h;
}let bloomFilter = new BloomFilter(10000, 0.01);bloomFilter.add("abcdefgh");
console.log(bloomFilter.contain("abcdefgh"));
console.log(bloomFilter.contain("abcdefghi"));

附:其它实现

  • 布隆过滤器 Python 实现示例
  • 高性能布隆过滤器 Python 实现示例
  • 布隆过滤器 Java 实现示例 1
  • 布隆过滤器 Java 实现示例 2

常见案例

  1. 比特币网络
  2. 分布式系统(Map-Reduce) — Hadoop、search engine
  3. Redis 缓存
  4. 垃圾邮件、评论等的过滤

参考文章:使用布隆过滤器解决缓存击穿、垃圾邮件识别、集合判重

部分图片来源于网络,版权归原作者,侵删。

18.布隆过滤器的实现及应用相关推荐

  1. 面试官问:什么是布隆过滤器?

    布隆过滤器 布隆过滤器是一种由位数组和多个哈希函数组成概率数据结构,返回两种结果 可能存在 和 一定不存在. 布隆过滤器里的一个元素由多个状态值共同确定.位数组存储状态值,哈希函数计算状态值的位置. ...

  2. Redis中布隆过滤器的使用及原理

    <玩转Redis>系列文章主要讲述Redis的基础及中高级应用.本文是<玩转Redis>系列第[11]篇,最新系列文章请前往公众号"zxiaofan"查看, ...

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

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

  4. 布隆过滤器及其数学推导

    目录 布隆过滤器 什么是布隆过滤器 原理简介 数学推导 空间占用情况 布隆过滤器 昨天突然看到了一个布隆过滤器的介绍和一些用法,感觉很新奇,也很有意思,刚好趁着周末来写一篇博客. 什么是布隆过滤器 布 ...

  5. 布隆过滤器:一种低空间成本的判断元素是否存在的方式

    简介 布隆过滤器(BloomFilter)是一种用于判断元素是否存在的方式,它的空间成本非常小,速度也很快. 但是由于它是基于概率的,因此它存在一定的误判率,它的Contains()操作如果返回tru ...

  6. 布隆过滤器(Bloom Filter)初探

    布隆过滤器介绍 布隆过滤器(Bloom Filter,下文简称BF)由Burton Howard Bloom在1970年提出,是一种空间效率高的概率型数据结构.它专门用来检测集合中是否存在特定的元素. ...

  7. 大白话布隆过滤器,又能和面试官扯皮了~

    前言 近期在做推荐系统中已读内容去重的相关内容,刚好用到了布隆过滤器,于是写了一篇文章记录分享一下. 文章的篇幅不是很长,主要讲了布隆过滤器的核心思想,目录如下: 什么是布隆过滤器? 布隆过滤器是由一 ...

  8. 算法学习 (门徒计划)3-2 哈希表与布隆过滤器及经典问题 学习笔记

    算法学习 (门徒计划)3-2 哈希表与布隆过滤器及经典问题 学习笔记 前言 哈希表 哈希操作 冲突处理 开放定址法 再哈希法 公共溢出区 链式地址法 扩容哈希表 设计简易哈希表 总结 布隆过滤器 对比 ...

  9. 玩转Redis-Redis中布隆过滤器的使用及原理

      <玩转Redis>系列文章主要讲述Redis的基础及中高级应用.本文是<玩转Redis>系列第[11]篇,最新系列文章请前往公众号"zxiaofan"查 ...

  10. 布谷鸟过滤器:实际上优于布隆过滤器

    Feb 10, 2021 布谷鸟过滤器:实际上优于布隆过滤器 本文译自原论文:https://www.cs.cmu.edu/~dga/papers/cuckoo-conext2014.pdf 摘要 在 ...

最新文章

  1. 手动部署OpenStack环境(四:安装控制器必备软件)
  2. python前缀表达式求值_python数据结构与算法 11 后缀表达式求值
  3. shell-1.shell注释
  4. 判断子序列不同的子序列两个字符串的删除操作编辑距离
  5. Python 小白从零开始 PyQt5 项目实战(7)折叠侧边栏的实现
  6. 关于我,十九线程序员小 UP
  7. golang编译工具LiteIDE的调试使用方法(F5)
  8. Sun Oracle服务器做磁盘raid
  9. java 多线程数据分发_多线程分发处理List集合数据
  10. 网络热词下的民意传播
  11. Linux netstat常用命令
  12. 【转】互联网架构的三板斧
  13. Asp.net MVC3中进行自定义Error Page
  14. 【开源】EasyDarwin编译全过程:Linux系统下编译运行最新版EasyDarwin的步骤介绍
  15. VS简明教程(VS安装、系统建议设置、软件设置、新建工程、发行、编译建议)、VS Code简明教程(安装、汉化、配置python环境、安装插件、新建并运行python程序)
  16. 网络安全界巨擘 王江民
  17. 桌面被关闭,如何在任务管理器中打开桌面?
  18. 找不到网站的服务器 dns 地址,为什么网站一直显示找不到服务器DNS地址?
  19. Ubuntu Server 安装Nginx 实例
  20. 大数据第三季--Hive(day3)-徐培成-专题视频课程

热门文章

  1. atiny_log | LiteOS 物联网操作系统中的日志打印组件使用分享
  2. xshell xftp 工具免费版本免费下载
  3. java se下载完怎么启动_【Java SE】如何安装JDK以及配置Java运行环境
  4. 《Java程序性能优化》、让你的Java程序更快、更稳定(PDF篇)
  5. c语言计算器括号怎么解决,C语言计算器,该如何解决
  6. 视频教程-带你入门matlab小波分析-Matlab
  7. ArcgisPro3.0.1中文安装包下载及安装教程
  8. Excel VBA编程教程(基础一)
  9. Excel、Word VBA 学习笔记
  10. 2010 模板下载 罗斯文_利用模板建立Access 2010数据库的方法