布隆过滤器(Bloom Filter)- 原理、实现和推导

目录

算法背景

布隆过滤器–概念

布隆过滤器—原理

布隆过滤器—缺点

布隆过滤器—实现

布隆过滤器—应用

布隆过滤器—公式推导


算法背景


问题:

在开发中,经常要判断一个元素是否在一个集合中。

实现方案:

编程中通常使用集合来存储所有元素,然后通过hash值来确定元素是否存在。

如:java中的HashMap、HashSet等。

优点:快速准确

缺点:耗费存储空间

瓶颈:

当集合比较小时,这个问题不明显

当集合比较大时,散列表存储效率低的问题越明显

如:判断邮件地址是否是发送垃圾邮件的地址

采用散列表:将每一个Email地址对应成一个8字节的信息指纹,然后存入散列表,由于散列表的存储效率一般只有50%,因此一个Email地址需要16个字节。一亿Email约1.6GB内存,存储几十亿个地址约上百GB的内存。

 

布隆过滤器–概念


布隆过滤器(英语:Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询效率都远远超过一般的算法,缺点是有一定的误判率和删除困难。

 

布隆过滤器—原理


布隆过滤器的原理是:

当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。

 

布隆过滤器—缺点


bloom filter之所以能做到在时间和空间上的效率比较高,是因为牺牲了判断的准确率、删除的便利性。

存在误判。可能要查到的元素并没有在容器中,但是hash之后得到的k个位置上值都是1。如果bloom filter中存储的是黑名单,可以通过建立一个白名单来存储可能会误判的元素。

删除困难。一个放入容器的元素映射到bit数组的k个位置上是1,删除的时候不能简单的直接置为0,可能会影响其它元素的判断。可以采用Counting Bloom Filter(计数布隆过滤器),将标准Bloom Filter位数组的每一位扩展为一个小的计数器(Counter)。

 

布隆过滤器—实现


在使用bloom filter时,绕不过的两点:

1)预估数据量n

2)期望的误判率fpp

在实现bloom filter时,绕不过的两点:

1)hash函数的选取

2)bit数组的大小

对于一个确定的场景,我们预估要存的数据量为n,期望的误判率为fpp,然后需要计算我们需要的Bit数组的大小m,以及hash函数的个数k,并选择hash函数。

(1)Bit数组大小选择

  根据预估数据量n以及误判率fpp,bit数组大小的m的计算方式:

(2)哈希函数选择

  由预估数据量n以及bit数组长度m,可以得到一个hash函数的个数k:

哈希函数的选择对性能的影响是很大的,一个好的哈希函数要能近似等概率的将字符串映射到各个Bit。选择k个不同的哈希函数比较麻烦,一种简单的方法是选择一个哈希函数,然后送入k个不同的参数。

下面使用java来实现布隆过滤器:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.BitSet;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

/**
 * 布隆过滤器
 * 
 * @author Administrator
 *
 */
public class BloomFilterDemo implements Serializable {

	private static final long serialVersionUID = -5221305273707291280L;

	private int dataCount; 			// 预期数要存的数据量
	private double falsePositive; 	// 期望的误判率

	private BitSet bits; 			// 位数组,使用BitSet实现
	private int bitSize;			// 位数组大小
	private int hashFunctionCount;	// 散列函数的个数

	public BloomFilterDemo(int dataCount, double falsePositive) {
		super();
		this.dataCount = dataCount;
		this.falsePositive = falsePositive;
		this.init();
	}

	/**
	 * 初始化
	 */
	private void init() {
		this.bitSize = (int) this.getNumOfBits(this.dataCount, this.falsePositive);
		this.hashFunctionCount = this.getNumOfHashFunctions(this.dataCount, this.bitSize);
		this.bits = new BitSet(this.bitSize);
	}

	/**
	 * 往布隆过滤器添加数据标记
	 * 如果不存在就进行记录并返回false,如果存在了就返回true
	 * 
	 * @param data
	 * @return
	 * @throws NoSuchAlgorithmException 
	 */
	public boolean add(String data) throws NoSuchAlgorithmException {

		int[] indexs = new int[this.hashFunctionCount];
		// 先假定存在
		boolean exist = true;
		int index;

		for (int i = 0; i < this.hashFunctionCount; i++) {
			indexs[i] = index = hash(data, i);
			if (exist) {
				if (!this.bits.get(index)) {
					// 只要有一个不存在,就可以认为整个字符串都是第一次出现的
					exist = false;
					// 补充之前的信息
					for (int j = 0; j <= i; j++) {
						//将对应的位设置为true
						this.bits.set(indexs[j], true);
					}
				}
			} else {
				//将对应的位设置为true
				this.bits.set(index, true);
			}
		}

		return exist;
	}

	/**
	 * 检查数据是否存在
	 * 
	 * @param data
	 * @return
	 * @throws NoSuchAlgorithmException 
	 */
	public boolean check(String data) throws NoSuchAlgorithmException {
		for (int i = 0; i < this.hashFunctionCount; i++) {
			int index = hash(data, i);
			if (!this.bits.get(index)) {
				return false;
			}
		}
		return true;
	}

	
	/**
	 * md5实现hash--目前测试,该hash算法的效果比较好
	 * @param message
	 * @param funNum
	 * @return
	 * @throws NoSuchAlgorithmException 
	 */
	private int hash(String message, int funNum) throws NoSuchAlgorithmException{
		MessageDigest md5 = MessageDigest.getInstance("md5");
        message  = message + String.valueOf(funNum);
        byte[] bytes = message.getBytes();
        md5.update(bytes);
        BigInteger bi = new BigInteger(md5.digest());

        return Math.abs(bi.intValue()) % this.bitSize;
    }


	/**
	 * 获取Bit数组的大小
	 * 
	 * @param n
	 *            预估要存的数据量
	 * @param p
	 *            期望的误判率
	 * @return
	 */
	private long getNumOfBits(long n, double p) {
		if (p == 0) {
			p = Double.MIN_VALUE;
		}
		// -1 * (n * log(p)) / (log(2) * log(2))
		return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
	}

	/**
	 * 获取hash函数的数量
	 * 
	 * @param n
	 *            预估要存的数据量
	 * @param m
	 *            Bit数组的大小m
	 * @return
	 */
	private int getNumOfHashFunctions(long n, long m) {
		// (m / n) * log(2)
		return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
	}

	/**
	 * 持久化布隆过滤器对象
	 * 
	 * @param path
	 */
	public void saveFilterToFile(String path) {
		try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(path))) {
			oos.writeObject(this);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}

	}

	/**
	 * 读取布隆过滤器对象
	 * 
	 * @param path
	 */
	public static BloomFilterDemo readFilterFromFile(String path) {
		try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path))) {
			return (BloomFilterDemo) ois.readObject();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public static void main(String[] args) {
		String file = "d:\\bloomFilter.obj";
		try {
			createFilter(file);
			readFilter(file);
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		
	}
	
	public static void createFilter(String file) throws NoSuchAlgorithmException {
		int count = 1000 * 10000;
		double fpp = 0.00001;
		
		System.out.println("start --- " + new Date());
		BloomFilterDemo bloomFilter = new BloomFilterDemo(count, fpp);
		int i = 0;
		while (i < count) {
			String msg = "时间:2018-10-01 10:00:00, 源IP:10.1.1.12,目标IP:192.1.1.205, 攻击类型:ddos攻击 -- " + i; 
			bloomFilter.add(msg);
			i++;
		}
		bloomFilter.saveFilterToFile(file);
		System.out.println("end --- " + new Date());
	}
	
	public static void readFilter(String file) throws NoSuchAlgorithmException {
		BloomFilterDemo bloomFilter = readFilterFromFile(file);
		System.out.println("bitSize: " + bloomFilter.bitSize);
		System.out.println("hashFunctionCount: " + bloomFilter.hashFunctionCount);

		System.out.println("start --- " + System.currentTimeMillis());
		int existCount = 0;
		for (int i = 0, size = 10 * 10000; i < size; i++) {
			String msg = "时间:2018-10-01 10:00:00, 源IP:10.1.1.12,目标IP:192.1.1.205, 攻击类型:ddos攻击 -- " + i;
			msg += System.currentTimeMillis();
			
			if (bloomFilter.check(msg)) {
				existCount++;
			}
		}
		System.out.println("end --- " + System.currentTimeMillis());
		System.out.println(existCount);
	}

}

 

布隆过滤器—应用


常见的几个应用场景:

(1)网页爬虫对URL的去重,避免爬取相同URL地址

(2)反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱(同理,垃圾短信)

(3)缓存击穿,将已存在的缓存放到布隆中,当黑客访问不存在的缓存时迅速返回避免缓存及DB挂掉

 

布隆过滤器—公式推导


预估要存的数据量为:n

期望的误判率为:P

Bit数组的大小为:m

Hash函数的个数为:k

推导过程:

1)对某一特定bit位在一个元素由某特定hash函数插入时没有被置为1的概率为:

2)则k个hash函数都没有将其置为1概率为:

3)如果插入了n个元素,都未将其置为1的概率为:

4)反过来,则此位被置为1的概率为:

5)一个不在集合中的元素,被误判在集合中的概率:

6)根据自然常数公式: lim(1+1/x)^x, x→∞,得出:

7)k为何值时可以使得误判率最低。设误判率为k的函数:

8)设:

9)则简化为:

10)两边取对数:

11)两边对k求导:

12)下面求最值:

则误判率最低时,得出k值:

13)把k代入误判率公式,得出:

14)把k代入误判率公式,得出m值:

 

  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
布隆过滤器是一种非常高效的数据结构,用于判断某个元素是否存在于一个集合中。它的基本原理是使用多个哈希函数对元素进行哈希,然后将哈希结果映射到一个位数组中的若干个位置,将这些位置标记为1。当需要查询某个元素是否在集合中时,将该元素进行哈希,判断哈希结果映射到的位数组上的值是否都为1,如果都为1,则说明该元素可能存在于集合中,但如果有任何一个位置为0,则说明该元素一定不存在于集合中。 布隆过滤器的应用场景非常广泛,例如网络爬虫中的URL去重、拼写检查、垃圾邮件过滤等。它的主要优点是占用空间非常小,而且查询速度非常快。 下面是一个简单的布隆过滤器实现示例: ```python import hashlib class BloomFilter: def __init__(self, m, k): self.m = m # 位数组的长度 self.k = k # 哈希函数的个数 self.bit_array = [0] * m # 初始化位数组 def add(self, key): for i in range(self.k): # 使用不同的哈希函数进行哈希 hash_val = int(hashlib.md5(str(key).encode('utf-8') + str(i).encode('utf-8')).hexdigest(), 16) # 将哈希结果映射到位数组上的若干个位置 pos = hash_val % self.m self.bit_array[pos] = 1 def contains(self, key): for i in range(self.k): hash_val = int(hashlib.md5(str(key).encode('utf-8') + str(i).encode('utf-8')).hexdigest(), 16) pos = hash_val % self.m if self.bit_array[pos] == 0: return False return True ``` 在上述代码中,我们使用了MD5哈希函数对字符串进行哈希,产生一个128位的哈希值,并将其转换为一个整数。然后,我们将这个整数对位数组的长度取模,得到一个在0到`m-1`之间的整数,将位数组上对应的位置标记为1。在查询元素是否存在于集合中时,我们同样对该元素进行k次哈希,并检查位数组上对应的位置是否都为1。如果有任何一个位置为0,则说明该元素一定不存在于集合中。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值