这个Kata其实几天前就完成了,不过最近一直在忙留学申请的事,没顾上写文章。
这次的内容就一个:实现一个布隆过滤器。
布隆过滤器(Bloom Filter)
什么是布隆过滤器呢?简单来说,布隆过滤器可以告诉你一个元素是否在一个集合中。
有同学可能就会说了,这很简单啊,集合中本来就有所有的元素,我只要检查输入内容是否在里面不就行了。
没错,这种方法成功率最高(100%),但是效率最低。
如果只是处理十个字符串、一百个字符串,这都没问题,但是如果要处理成百上千万的字符串,显然是不可行的,空间和时间都无法接受。
这时候该主角出场了——布隆过滤器。
布隆过滤器的核心思想就是用尽可能少的空间来存储尽可能多的内容,同时尽量保证准确率。布隆过滤器本质上很像哈希表,把一个大的空间映射到一个小的空间,那么问题就来了,如果两个不同元素映射到了同一个位置怎么办呢?哈希表有各种算法可以解决冲突,但是布隆过滤器非常简单粗暴——我才不管你,映射到同一个位置那就存同一个位置。
因此,布隆过滤器的准确率并不是100%。准确地说,布隆过滤器具有假阳性,也就是说如果它告诉你一个元素在集合中,那这个结果可能是错误的,因为有可能这个元素和集合中另一个不同的元素映射到了同一位置。但是反过来,如果它告诉你一个元素不在集合中,那这个元素就肯定不在集合中。所以布隆过滤器在某些场景下是非常合适的。
为了进一步压缩空间同时提高准确率,布隆过滤器有很多种改进方法,其中比较常用的是把一个元素映射到结果集中的多个位(bit)。举例来说,假设每个元素会被映射到5个点,那么两个元素只要有一个点不相同就可以判断出来;但是如果只映射到一个点,那么这个点只要相同就无法准确判断了,因此映射到多个点可以增加准确度。此外,这个方法还可以压缩空间,因为映射一个点的时候,为了减少碰撞我们必须使用很大的空间,但是映射多个点的时候,多个点全部相同的概率很小,所以我们就可以使用比较小的空间。
代码实现
我用Python实现了一个简易的布隆过滤器,代码如下
import hashlib
import struct
import math
def readWords(filename):
result = []
with open(filename) as f:
for i in f.readlines():
result.append(i.strip())
return result
def getNBits(hashedWord, mapLen, bitNumber):
binaryHash = "".join(["0" * (4 - len(bin(int(i, 16))[2:])) + bin(int(i, 16))[2:] for i in hashedWord])
wordLen = len(binaryHash)
if bitNumber > wordLen:
bitNumber = wordLen
gap = min(wordLen / bitNumber, mapLen)
return [int(binaryHash[i * gap: (i + 1) * gap], 2) for i in range(bitNumber)]
def buildBloomFilter(all_words, mapLen, bitNumber):
bitmap = [0 for i in range(2**mapLen)]
def assignBitMap(i):
bitmap[i] = 1
for i in all_words:
map(assignBitMap, getNBits(hashlib.md5(i).hexdigest(), mapLen, bitNumber))
return bitmap
def lookUpBitMap(words, bitmap, mapLen, bitNumber):
for i in getNBits(hashlib.md5(words).hexdigest(), mapLen, bitNumber):
if bitmap[i] == 0:
return False
return True
mapLen = 20
bitNumber = 4
bitmap = buildBloomFilter(readWords("/usr/share/dict/words"), mapLen, bitNumber)
print lookUpBitMap("asdasd", bitmap, mapLen, bitNumber) # false
print lookUpBitMap("chinanb", bitmap, mapLen, bitNumber) # false
说实话用Python进行位操作确实不太方便。。。
代码倒数四五行的mapLen
和bitNumber
这两个参数是核心,mapLen
是最终存储空间的长度(位),bitNumber
是映射点的数量。代码的思路很简单,先把字符串用md5编码,然后转换成二进制并取bitNumber
段二进制数,最后把bitmap
中这几段二进制数对应的十进制下标设置为1。检测的时候,先计算出bitNumber
段二进制数,然后检查bitmap
,如果这几个位置全是1说明(可能)在集合中,只要有一个不是1就说明肯定不在集合中。
压缩率计算
通过代码大家也可以看出,布隆过滤器的思路其实很简单,关键是参数调优,这里我们是手工选择了几个参数进行测试,这方面肯定有很多相关论文和算法,大家感兴趣的话可以去搜一下。
在我们的代码中,mapLen
是20,也就是说存储空间是2的20次方位,等于2的17次方字节,也就是128KB。而/usr/share/dict/words
这个文本文件一共2.4MB,也就是2400KB,压缩率达到95%!!!
不过这里还是提一下,我们的参数只是手工调整出来的,也没有严格地测量准确率,所以只是一个参考。可能实际使用中压缩率没有这么高,也可能比这个还要高,我们只是进行一些定性的测试。
总之,布隆过滤器实在是太强了,在特定场景下可以起到极大的作用!