Fork me on GitHub

【译】Swift算法俱乐部-布隆过滤器

本文是对 Swift Algorithm Club 翻译的一篇文章。
Swift Algorithm Clubraywenderlich.com网站出品的用Swift实现算法和数据结构的开源项目,目前在GitHub上有18000+⭐️,我初略统计了一下,大概有一百左右个的算法和数据结构,基本上常见的都包含了,是iOSer学习算法和数据结构不错的资源。
🐙andyRon/swift-algorithm-club-cn是我对Swift Algorithm Club,边学习边翻译的项目。由于能力有限,如发现错误或翻译不妥,请指正,欢迎pull request。也欢迎有兴趣、有时间的小伙伴一起参与翻译和学习🤓。当然也欢迎加⭐️,🤩🤩🤩🤨🤪。
本文的翻译原文和代码可以查看🐙swift-algorithm-club-cn/Bloom Filter


布隆过滤器(Bloom Filter)

介绍

布隆过滤器是一种节省空间的数据结构,可以告诉您元素是否存在于集合中。

这是一个概率数据结构:对布隆过滤器的查询返回false,意味着该元素肯定不在集合中,或者是true,这意味着元素可能在集合中。

误报的可能性很小,即使查询返回true,元素实际上也可能不在集合中。 但是永远不会有任何漏报:如果查询返回false,你可以保证,那么元素确实不在集合中。

所以布隆过滤器告诉你,“绝对不是”或“可能是的”。

起初,这似乎不太有用。 但是,它在缓存过滤和数据同步等应用程序中很重要。

布隆过滤器优于哈希表的一个优点是前者保持恒定的内存使用和恒定时间插入和搜索。 对于具有大量元素的集合,哈希表和布隆过滤器之间的性能差异很大,如果您不需要保证不存在误报,则它是可行的选项。

注意:与哈希表不同,布隆过滤器不存储实际对象。 它只会记住你看过的对象(有一定程度的不确定性)以及你没有看过的对象。

将对象插入集合中

布隆过滤器本质上是一个固定长度的位向量,一个位数组。 当我们插入对象时,我们将其中一些位设置为1,当我们查询对象时,我们检查某些位是0还是1。 两个操作都使用哈希函数。

要在过滤器中插入元素,可以使用多个不同的哈希函数对元素进行哈希。 每个哈希函数返回一个我们映射到数组中索引的值。 然后,我们将这些索引处的位设置为1true

例如,假设这是我们的位数组。 我们有17位,最初它们都是0false

[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]

现在我们要在布隆过滤器中插入字符串"Hello world!"。 我们对此字符串应用两个哈希函数。第一个给出值1999532104120917762。我们通过取数组长度的模数将此哈希值映射到数组的索引:1999532104120917762 % 17 = 4。 这意味着我们将索引4处的位设置为1或者true

[ 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]

然后我们再次散列原始字符串,但这次使用不同的散列函数。 它给出哈希值9211818684948223801。取17的模数为12,我们也将索引12处的位设置为1

[ 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 ]

这两个1位足以告诉布隆过滤器它现在包含字符串 "Hello world!"。 当然,它不包含实际的字符串,所以你不能要求布隆过滤器,“给我一个你包含的所有对象的列表”。 所有它都是一堆1和0。

查询集合

类似于插入,查询是通过首先对期望值进行哈希来实现的,该期望值给出几个数组索引,然后检查这些索引处的所有位是否为1。 如果其中一个位不是1,则无法插入该元素,并且查询返回false。 如果所有位都是1,则查询返回true

例如,如果我们查询字符串"Hello WORLD",那么第一个哈希函数返回5383892684077141175,其中取17的模是12。该位是1。但是第二个哈希函数给出5625257205398334446,它映射到数组索引9。该位为0。 这意味着字符串"Hello WORLD"不在过滤器中,查询返回false

第一个哈希函数映射到1位的事实是巧合(它与两个字符串以"Hello "开头的事实无关)。 太多这样的巧合可能导致“碰撞”。 如果存在冲突,即使未插入元素,查询也可能错误地返回true - 导致前面提到的误报问题。

假设我们插入了一些其他元素,"Bloom Filterz",它设置了第7位和第9位。现在数组看起来像这样:

[ 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0 ]

如果再次查询"Hello WORLD",则过滤器会看到第12位为true,第9位现在也为true。 它报告说"Hello WORLD"确实出现在集合中,即使它不是……因为我们从未插入过那个特定的字符串。这是误报。这个例子说明了为什么布隆过滤器永远不会说“绝对是”,只有“可能是”。

您可以通过使用具有更多位的数组并使用其他哈希函数来解决此类问题。 当然,使用的哈希函数越多,布隆过滤器就越慢。 所以你必须取得平衡。

使用布隆过滤器无法删除,因为任何一个位都可能属于多个元素。 一旦你添加了一个元素,它就在那里。

布隆过滤器的性能是O(k),其中 k是哈希函数的数量。

代码

代码非常简单。 内部位数组在初始化时设置为固定长度,初始化后不能进行突变。

1
2
3
4
public init(size: Int = 1024, hashFunctions: [(T) -> Int]) {
self.array = [Bool](repeating: false, count: size)
self.hashFunctions = hashFunctions
}

应在初始化时指定几个哈希函数。 您使用哪些哈希函数将取决于您将添加到集合的元素的数据类型。 你可以在playground测试中看到一些例子 - 字符串的djb2sdbm哈希函数。

插入只是将所需的位翻转为true

1
2
3
4
5
public func insert(_ element: T) {
for hashValue in computeHashes(element) {
array[hashValue] = true
}
}

这使用computeHashes()函数,它循环遍历指定的hashFunctions并返回索引数组:

1
2
3
private func computeHashes(_ value: T) -> [Int] {
return hashFunctions.map() { hashFunc in abs(hashFunc(value) % array.count) }
}

并查询检查以确保哈希值处的位为true

1
2
3
4
5
6
public func query(_ value: T) -> Bool {
let hashValues = computeHashes(value)
let results = hashValues.map() { hashValue in array[hashValue] }
let exists = results.reduce(true, { $0 && $1 })
return exists
}

如果你来自另一种命令式语言,你可能会注意到exists赋值中的不寻常语法。 当Swift使代码更加简洁和可读时,Swift使用函数范例,在这种情况下,reduce是一种更加简洁的方法来检查所有必需的位是否为true而不是用for循环。

作者:Jamil Dhanani,Matthijs Hollemans
翻译:Andy Ron
校对:Andy Ron

坚持原创技术分享,您的支持将鼓励我继续创作!