[算法系列] - bitmap 和 布隆过滤器

1,326 阅读3分钟

我们有 1 千万个整数,整数的范围在 1 到 1 亿之间。如何快速查找某个整数是否在这 1 千万个整数中呢?

当然,这个问题还是可以用散列表来解决。不过,我们可以使用一种比较“特殊”的散列表,那就是位图。我们申请一个大小为 1 亿、数据类型为布尔类型(true 或者 false)的数组。我们将这 1 千万个整数作为数组下标,将对应的数组值设置成 true。比如,整数 5 对应下标为 5 的数组值设置为 true,也就是 array[5]=true。

当我们查询某个整数 K 是否在这 1 千万个整数中的时候,我们只需要将对应的数组值 array[K]取出来,看是否等于 true。如果等于 true,那说明 1 千万整数中包含这个整数 K;相反,就表示不包含这个整数 K。

不过,很多语言中提供的布尔类型,大小是 1 个字节的,并不能节省太多内存空间。实际上,表示 true 和 false 两个值,我们只需要用一个二进制位(bit)就可以了。那如何通过编程语言,来表示一个二进制位呢?

Python 代码实现:

from typing import Optional

class Bitmap:
    def __init__(self, num_bits: int):
        self._num_bits = num_bits
        self._bytes = bytearray(num_bits // 8 + 1)
    
    def setbit(self, k: int) -> None:
        if k > self._num_bits: return
        self._bytes[k // 8] |= (1 << k % 8)
    
    def getbit(self, k: int) -> Optional[bool]:
        if k > self._num_bits: return
        return self._bytes[k // 8] & (1 << k % 8) != 0

if __name__ == "__main__":
    bitmap = Bitmap(1000000)
    bitmap.setbit(1)
    bitmap.setbit(3)
    bitmap.setbit(6)
    bitmap.setbit(7)
    bitmap.setbit(8888)

    for i in range(0, 11):
        print(bitmap.getbit(i))
    
    print(bitmap.getbit(8888))

如果用散列表存储这 1 千万的数据,数据是 32 位的整型数,也就是需要 4 个字节的存储空间,那总共至少需要 40MB 的存储空间。如果我们通过位图的话,数字范围在 1 到 1 亿之间,只需要 1 亿个二进制位,也就是 12MB 左右的存储空间就够了。

但如果数字的范围很大,在 1 到 10 亿之间,那位图的大小就是 10 亿个二进制位,也就是 120MB 的大小,消耗的内存空间,不降反增。

散列表存的是已有数据,位图存的是范围,所以范围大数据少时,散列表反而更节省空间。

这个时候,布隆过滤器就要出场了。

数据个数是 1 千万,数据的范围是 1 到 10 亿。布隆过滤器的做法是,我们仍然使用一个 1 亿个二进制大小的位图,然后通过哈希函数,对数字进行处理,让它落在这 1 到 1 亿范围内。比如我们把哈希函数设计成 f(x)=x%n。其中,x 表示数字,n 表示位图的大小(1 亿),也就是,对数字跟位图的大小进行取模求余。

为了降低冲突概率,我们使用 K 个哈希函数,对同一个数字进行求哈希值,那会得到 K 个不同的哈希值,我们分别记作 X1,X2,X3,…,XK。存储和查询如图所示:

同时也会造成误判:

布隆过滤器的误判有一个特点,那就是,它只会对存在的情况有误判。如果某个数字经过布隆过滤器判断不存在,那说明这个数字真的不存在,不会发生误判;如果某个数字经过布隆过滤器判断存在,这个时候才会有可能误判,有可能并不存在。

False is always false. True is maybe true.