新来的小区保安阿强接到一个任务:记住每一位访客是否来过。他不用相册、不记姓名,只在门口挂一块格子木板。来客报上身份特征,他拿出两枚印章,在对应的两个格子上各按一下。第二天,只要看这两格有没有章痕就足够判断。神奇的是,阿强的木板从不把真正来过的人拦在门外,却偶尔会放进一位根本没来过的人——因为某个陌生人该有的章痕,恰好被之前的访客盖满了。这就是布隆过滤器:用极小的空间记录“存在过的痕迹”,用可接受的误判换来惊人的内存效率。我们要聊的,正是这块木板背后的数学与工程。

岗位关联

掌握布隆过滤器,相当于给后端系统配上一道成本极低的前置防线;无论你是优化缓存、做爬虫去重,还是准备面试,它都是高频出现、值得吃透的“空间换时间”利器。

目录
  1. 可视化演示
  2. 核心原理
  3. 哈希映射
  4. 位数组操作
  5. 误判率分析
  6. 参数设计
  7. 计数过滤器
  8. 缓存穿透
  9. 爬虫去重
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示布隆过滤器核心结构(m位位数组+k哈希函数)、误判率数学分析(FPR公式与最优k值)、以及实战应用(缓存穿透防护+URL去重+计数布隆过滤器)。

本节配有交互动效(进入视口后自动加载,约 3KB)。

'+captions.struct+'

核心原理

小区新来的保安阿强,被安排在门口守一块木板。木板有20个格子,平时全空。来过的人,阿强不记名字,只按“身份证末位数字”和“身高厘米数取个位”两个规则,在对应的两个格子各盖一个章。第二天又来一位客人,阿强看他的信息,如果对应两个格子里章都齐,就放行;如果有格子空着,礼貌拦下。这块木板,就是布隆过滤器的实体模型。

木板是位数组,两个规则是哈希函数。位数组长度记作 $m$,初始每一位都是 0;元素加入集合时,用 $k$ 个哈希函数算出 $k$ 个位置并置为 1;查询元素时,检查这 $k$ 个位置是否全是 1。布隆过滤器是一种概率型数据结构,不存储元素本身,只存储“有没有被标记过”的痕迹,因此空间开销与元素大小无关,只与 $m$ 和 $k$ 有关。

布隆过滤器的插入和查询时间复杂度都是 $O(k)$,$k$ 通常是个小常数。最关键在于:只要有一个位置为 0,就能断定元素不在集合中;如果 $k$ 个位置都为 1,只能说“可能在”。这个“可能”的概率就是误判率 $p$ 的来源。

阿强不需要知道每个客人长什么样,但只要看到某位客人对应的格子里有一个空位,就能肯定这位客人之前没登记过。这种以少量痕迹代替完整名单的智慧,就是布隆过滤器最核心的“概率型集合”思想,也是它空间高效的根本原因。

B[i]=0,∀i∈{0,1,…,m−1}B[i] = 0, \quad \forall i \in \{0,1,\dots,m-1\}
位数组初始化为全零,之后只能通过哈希函数将若干位置为 1,这块“空白登记板”是后续一切操作的基础。
  1. 符号 B 与 i$B$ 表示长度为 $m$ 的位数组,$i$ 是该数组的下标,取值范围为 $0$ 到 $m-1$。
    B[i]=0B[i] = 0
  2. 全零初始化$B[i] = 0$ 表示每个位都从“没有盖过章”开始,这是后续插入和查询的前提。
    ∀i∈{0,1,…,m−1}\forall i \in \{0,1,\dots,m-1\}
  3. 全称量词含义$\forall$ 表示“对每一个”,即整块登记板在上班前所有格子都是空白的。
    ∀i\forall i

哈希映射

阿强用“身份证末位”和“身高厘米数取个位”作为两个盖章规则。规则不同,同一客人得到的章印位置也不同;不同客人也可能盖到同一格。规则设计得越均匀,章印越分散,误判就越少。这些规则,在布隆过滤器里就是哈希函数。

在布隆过滤器中,有 $k$ 个独立的哈希函数 $h_1, \dots, h_k$,每个函数将任意元素映射到 $[0, m-1]$ 的一个整数下标。因为输出范围只有 $m$ 个格子,而输入元素往往远远大于 $m$,所以多个元素映射到同一格是必然事件,这些“盖章撞车”正是误判率的根源之一。

好的哈希函数应当尽量均匀、独立,工程上常使用 CRC32、MurmurHash 等成熟算法。实际实现时,往往先用两个基础哈希得到两个值,再通过线性组合派生出 $k$ 个位置,不必真的实现 $k$ 个完全不同的哈希函数。每个哈希计算都是 $O(1)$,所以总复杂度为 $O(k)$。

就像多个不同形状的印章能让访客的“指纹”分布在板子各处,多个独立哈希函数让元素在位数组中尽量分散,减少局部拥挤,从而降低误判率。

hi:U→{0,1,…,m−1},i=1,2,…,kh_i: U \to \{0,1,\dots,m-1\}, \quad i=1,2,\dots,k
每个哈希函数都是一个“盖章规则”,它把元素映射到登记板上的一个格子位置。
  1. 定义域 U$U$ 是所有可能被加入集合的元素集合,可以是一段字符串、一个整数或一个 URL。
    UU
  2. 映射关系$h_i(x)$ 将元素 $x$ 映射为 $0$ 到 $m-1$ 之间的整数,对应登记板上其中一个格子。
    hi:U→{0,1,…,m−1}h_i: U \to \{0,1,\dots,m-1\}
  3. 函数个数$i=1,\dots,k$ 表示共有 $k$ 个这样的规则,每个元素需要同时在这 $k$ 个位置留下印记。
    i=1,2,…,ki=1,2,\dots,k

位数组操作

阿强的工作分两步:盖章(插入)和看章(查询)。登记来的访客,按规则在对应格子上盖红章;核验时,只检查对应格子的章是否都在。布隆过滤器的操作也完全对应:插入元素时,将 $k$ 个哈希位置全部置为 1;查询元素时,检查这 $k$ 个位置。

插入操作:对元素 $x$,逐个计算 $h_1(x) \dots h_k(x)$,将这些位置上的位设为 1。查询操作:对 $x$ 计算同样的 $k$ 个位置,只要发现任何一个位为 0,就返回“不在集合”;如果全部为 1,才返回“可能在集合”。由于只有置 1 操作、没有置 0 操作,经典布隆过滤器无法删除元素。

查询时使用逻辑与(AND)聚合 $k$ 个位,可以并行加速。置位操作是幂等的,重复插入同一元素不会改变数组状态。位数组可以用 bytearray、Bitmap 或硬件向量实现,性能极高。

回头看那块登记板,只要有一个必须盖的格子空着,章就没盖全,保安可以理直气壮拦人。这正是布隆过滤器“零假阴性”的由来:它从不把真正来过的人拦在门外,最多可能放错一两个陌生人。

Query(x)=⋀i=1kB[hi(x)]\text{Query}(x)=\bigwedge_{i=1}^{k} B[h_i(x)]
查询结果等于 $k$ 个位做“逻辑与”,只要有一个 0,结果就是“不在集合”。
  1. Query 函数含义$\text{Query}(x)$ 表示对元素 $x$ 是否可能在集合中的一次判定。
    Query(x)\text{Query}(x)
  2. 逻辑与运算$\bigwedge_{i=1}^{k}$ 相当于对 $i=1$ 到 $k$ 的所有条件做“与”运算,只有全部为 1 时才为真。
    ⋀i=1k\bigwedge_{i=1}^{k}
  3. 关键位表达$B[h_i(x)]$ 是第 $i$ 个哈希函数算出的位置上的位;只要其中有任何一个为 0,就说明元素一定不在。
    B[hi(x)]B[h_i(x)]

误判率分析

为什么“章都齐了”也不一定可靠?因为规则总数只有 $m$ 个格子,来的人多了,不同人的章会重叠。阿强可能看到一位从没来过的客人 A,他该盖的格子恰好都被之前的客人盖过,于是被误放进去。这就是误判:元素明明不在集合中,却因为位全部被占而判断为“存在”。

数学上,插入 $n$ 个元素后,某个特定位置仍为 0 的概率,等于 $kn$ 次哈希都没有选到它的概率。用极限近似可得 $(1-\frac{1}{m})^{kn} \approx e^{-kn/m}$,因此某个位置为 1 的概率约等于 $1-e^{-kn/m}$。查询一个新元素时,需要它的 $k$ 个位置同时为 1,所以误判率就是这个概率的 $k$ 次方。

误判率公式如下。注意这里的概率是在“元素并不在集合中”的前提下,布隆过滤器误认为它在的概率。

阿强如果想降低误放概率,就要换更大的板子(增大 $m$)、控制登记人数($n$),或调整章的数量($k$)。但三者互相牵制,并非越大或越多就越好,这正是下一节参数设计的核心问题。

p≈(1−e−kn/m)kp \approx \left(1 - e^{-kn/m}\right)^{k}
$p$ 是假阳性概率,也就是“没来过的人被误放行”的概率;它只会出现在不存在元素的判定中。
  1. 单位置概率$1-e^{-kn/m}$ 表示经过 $n$ 次插入后,一个固定位被置为 1 的近似概率。
    1−e−kn/m1-e^{-kn/m}
  2. 幂指数 k整个概率取 $k$ 次方,因为新元素的 $k$ 个哈希位置必须同时为 1 才会被判定为“存在”。
    (1−e−kn/m)k\left(1-e^{-kn/m}\right)^{k}
  3. 参数角色$m$ 是位数组长度,$n$ 是插入元素数,$k$ 是哈希函数个数;三者共同决定误判率。
    p≈(1−e−kn/m)kp \approx \left(1 - e^{-kn/m}\right)^{k}

参数设计

阿强盘算:木板要多大、章要几枚,才能让误放概率降到 1%?如果板子太大占地方,章太多反而把格子盖满。布隆过滤器设计时也需要回答同样的两个问题:给定预期元素数量 $n$ 和可接受的误判率 $p$,位数组长度 $m$ 和哈希函数个数 $k$ 各取多少?

通过误判率公式反推,可以得到最优参数。位数组长度满足 $m = -\frac{n \ln p}{(\ln 2)^2}$,而最优哈希函数数量为 $k = \frac{m}{n} \ln 2$。直觉上,最优 $k$ 使得插入完成后位数组中约一半为 1,此时误判率最低。

实际工程中,$m$ 和 $k$ 都要向上取整,再通过模拟验证。$m/n$ 是每个元素占用的位数,为了得到 1% 到 0.1% 的误判率,通常为每个元素分配 10 到 20 位。如果 $n$ 预估不准,实际元素数量超过设计预期,误判率会随之升高。

阿强最终选择了一块足够大但不过分的木板、恰到好处的印章数量。这也体现了布隆过滤器精髓:用可控的误差换空间,把参数算得明明白白。

m=−nln⁡p(ln⁡2)2,k=mnln⁡2m = -\frac{n \ln p}{(\ln 2)^2}, \quad k = \frac{m}{n} \ln 2
这两个公式把“预期元素数”和“能接受的误判率”翻译成具体的位数组长度和哈希函数个数。
  1. m 公式来源对误判率公式求最优并化简,得到 $m$ 与 $n$、$p$ 的关系;$-\ln p$ 表示对误判率要求越严格,需要的位数越多。
    m=−nln⁡p(ln⁡2)2m = -\frac{n \ln p}{(\ln 2)^2}
  2. 常数部分$(\ln 2)^2$ 由最优 $k=(m/n)\ln 2$ 回代误判率公式后产生。
    (ln⁡2)2(\ln 2)^2
  3. k 公式$k = \frac{m}{n} \ln 2$ 给出了让位数组半满的最优盖章数量。
    k=mnln⁡2k = \frac{m}{n} \ln 2

计数过滤器

阿强的板子只能记录“有没有章”,但有时候登记错了,想擦掉某个人的章。一擦才发现,这个格子可能也被别人盖过;直接擦会把别人的痕迹也抹掉。于是他把每个格子换成一块“计数器小黑板”,盖一次章就写 +1,擦一次就写 -1,只要数字大于 0 就表明还有人在用这个格子。

计数布隆过滤器正是这样:把位数组的每一位扩展成一个计数器(通常 4 位,最大计数 15)。插入时对应计数器加 1;查询时检查计数器是否大于 0;删除时对应计数器减 1。如果删除的元素确实被插入过,且没有错误共享,就能安全删除。

代价是空间:经典版本用 $m$ 位,计数版本用 $m \times c$ 位,$c$ 是计数器位宽,通常占用是原来的 4 倍。同时存在溢出风险:同一个格子计数可能超过计数器最大值,导致计数回绕;因此要选择足够位宽,并监控计数分布。计数布隆过滤器仍然不能枚举元素,只是支持删除而已。

阿强的“计数小黑板”确实灵活,但他很快发现,如果同一格被盖了太多次,小黑板会写不下——这就是计数器溢出的隐患。因此计数布隆过滤器适合需要删除、且元素数量波动不大的场景。

C[hi(x)]←C[hi(x)]+1,C[hi(x)]←C[hi(x)]−1C[h_i(x)] \leftarrow C[h_i(x)] + 1, \quad C[h_i(x)] \leftarrow C[h_i(x)] - 1
插入时计数器加一,删除时计数器减一;只要计数器大于 0,就表示该位仍有元素引用。
  1. 计数器数组 C$C$ 是长度为 $m$ 的计数器数组,$C[j]$ 表示第 $j$ 个格子上“被盖过的总次数”。
    C[j]C[j]
  2. 插入更新对元素 $x$ 的每个哈希位置 $h_i(x)$,计数器加 1,相当于在登记板上登记一次。
    C[hi(x)]←C[hi(x)]+1C[h_i(x)] \leftarrow C[h_i(x)] + 1
  3. 删除更新同样位置计数器减 1,只要计数不降到负值,就说明至少还有别的元素标记着该位。
    C[hi(x)]←C[hi(x)]−1C[h_i(x)] \leftarrow C[h_i(x)] - 1

缓存穿透

小区来了很多外卖员,总有一些人压根不住在小区。如果每位外卖员都去每一栋楼敲门问,整栋楼会烦死。阿强在门口先用登记板快速预检:板子说没见过,就直接拦下。数据库系统也面临同样问题,这就是缓存穿透。

缓存穿透指查询缓存和数据库都不存在的数据,每次请求都打到数据库,流量大时能把后端打垮。解决思路是在缓存前加一道布隆过滤器:若过滤器判定 key 不存在,直接返回“无数据”,不再访问数据库。由于布隆过滤器假阴性为 0,凡是真实存在的 key 一定不会被误拦。

但误判率会让少数不存在的请求穿过过滤器,所以通常与空值缓存配合:把查询结果为空也短暂缓存,双保险。当大流量热点来临时,布隆过滤器用几十 KB 到几 MB 内存就能挡住绝大多数无效请求。工程实现中,key 通常是商品 ID、用户 ID 等业务主键,哈希函数要选好。

阿强的登记板不是万能门禁,但它把绝大多数陌生人挡在门外,省去了楼上楼下的空跑。缓存系统有了它,就像在数据库门前站了一个记忆力极好、又从不记脸的保安。

If ¬BF(x) then return null, else query DB\text{If } \neg BF(x) \text{ then return null, else query DB}
先问登记板,板子说没有就直接返回空;板子说有才去数据库确认。
  1. BF(x) 查询$BF(x)$ 表示布隆过滤器对元素 $x$ 的查询结果,只有“可能在集合”和“一定不在集合”两种。
    BF(x)BF(x)
  2. 否定判断$\neg BF(x)$ 表示“过滤器判定不存在”,此时可以安全短路,不用再访问数据库。
    ¬BF(x)\neg BF(x)
  3. 流程逻辑否则进入数据库查询;布隆过滤器是“前置过滤层”,并不替代缓存本身。
    If ¬BF(x) then return null, else query DB\text{If } \neg BF(x) \text{ then return null, else query DB}

爬虫去重

假设阿强要去另一家公司当保安,登记板要换成能记住几亿客人名字的电子名单,成本极高。但公司只想知道“这个客人以前来过吗”,不知道名字也没关系——于是又用回盖章登记板。这正是爬虫去重场景:只需要判断 URL 是否访问过,不需要存下 URL 全文。

爬虫抓取网页时,需要判断一个 URL 是否已经访问过。如果把每个 URL 原样存进哈希集合,一个 100 字节的 URL,一亿条就要 10 GB 内存。布隆过滤器把 URL 映射到 $m$ 位,一亿条 URL 通常只需几百 MB 甚至几十 MB,内存省了一个量级。

去重场景对误判很宽容:误判只会让某个 URL 被当成“已访问”而跳过,导致漏抓页面,不会重复抓取造成污染,可以接受。分布式爬虫中,可以用固定 $m$ 和 $k$ 的布隆过滤器分片存在不同节点,或借助 Redis 的 bitmap 实现共享去重。

阿强笑了:记住“来过”不需要记住“你是谁”。只要回答是或否,一张板子就能覆盖海量 URL,让爬虫跑得又轻又快。

ratio=m8nL\text{ratio} = \frac{m}{8 n L}
这是布隆过滤器内存与朴素哈希表内存的粗略比值,$L$ 是 URL 平均字节长度。
  1. 分子含义$m$ 是位数组总位数,除以 8 得到字节数。
    m8\frac{m}{8}
  2. 分母含义$n \times L$ 表示存 $n$ 个 URL,每个平均 $L$ 字节时的原始存储量。
    nLn L
  3. 比值意义当 $m$ 远小于 $8 n L$ 时,布隆过滤器能大幅降低内存。
    ratio=m8nL\text{ratio} = \frac{m}{8 n L}

AI 应用场景

缓存穿透防护

阶段:数据访问与预处理

解决的问题:传统缓存方案在遇到大量不存在的 key 时会产生缓存穿透,数据库被无效请求淹没。布隆过滤器在数据访问层前置一个“访客登记板”,先判断 key 是否可能出现过,快速拦截不存在 key,将数据库查询量降低一个量级。

具体案例:某电商秒杀系统将商品 ID 加入布隆过滤器,大促期间恶意构造不存在的商品 ID 请求,拦截率超过 99%,数据库负载下降 80%。

爬虫URL去重

阶段:数据采集

解决的问题:普通哈希集合存储 URL 内存消耗大,内存成为瓶颈。布隆过滤器用固定位数组回答“这个 URL 访问过吗”,把上亿 URL 压缩到几千万位。

具体案例:新闻聚合爬虫每天新增 URL 5000 万条,用布隆过滤器把去重内存从数 GB 降到 200 MB,偶尔漏抓几个页面不影响整体数据质量。

Python 演示

python43 行
import mathimport zlib  class BloomFilter:    def __init__(self, n, p=0.01):        self.m = int(-n * math.log(p) / (math.log(2) ** 2))        self.k = int(self.m / n * math.log(2))        if self.k < 1:            self.k = 1        self.bits = bytearray(self.m)     def _hashes(self, item):        data = item.encode('utf-8')        h1 = zlib.crc32(data) & 0xffffffff        h2 = zlib.adler32(data) & 0xffffffff        return [(h1 + i * h2) % self.m for i in range(self.k)]     def add(self, item):        for pos in self._hashes(item):            self.bits[pos] = 1     def __contains__(self, item):        return all(self.bits[pos] == 1 for pos in self._hashes(item))  def demo():    n = 10000    bf = BloomFilter(n, 0.01)    for i in range(n):        bf.add('user-%d' % i)     false_positive = 0    trials = 10000    for i in range(trials):        if 'unknown-%d' % i in bf:            false_positive += 1     print('m=%d, k=%d, 实测误判率=%.4f%%' % (bf.m, bf.k, false_positive * 100.0 / trials))  if __name__ == '__main__':    demo()
目的:用标准库实现一个可用的布隆过滤器,并通过一万次虚假查询估算误判率。
  1. self.m = int(-n * math.log(p) / (math.log(2) ** 2))根据期望元素数量和误判率反推位数组长度,相当于计算登记板需要多少格子。
  2. self.k = int(self.m / n * math.log(2))算出最优印章数量,让误判率最低。
  3. return [(h1 + i * h2) % self.m for i in range(self.k)]用 CRC32 和 Adler32 两个基础哈希派生 k 个位置,避免实现 k 个不同哈希函数。
  4. self.bits[pos] = 1插入时把哈希位置对应的位改成 1,就像在登记板上盖章。
  5. all(self.bits[pos] == 1 for pos in self._hashes(item))查询时要求 k 个位置全都是 1,只要有一个 0 就说明元素不在。

常见误区

误区位数组本身就是 0 和 1,想删除一个元素时,把它对应的位改成 0 不就行了吗?为什么都说不能删除?

正解同一个位往往被多个元素共享。假设元素 A 和 B 都覆盖第 3 位,删除 A 时把第 3 位清零,B 就再也查不到了,相当于“漏拦”真元素,这会破坏布隆过滤器最核心的零假阴性承诺。计数布隆过滤器通过计数器加一减一解决删除问题,但空间成本会提高到原来的数倍。

为何常见:我们习惯把“位”看成每个元素私有的标签,忽略了哈希冲突带来的共享。

误区既然是概率型结构,那它肯定既可能误报也可能漏报,真正在集合里的元素也可能查不到。

正解布隆过滤器只有假阳性、没有假阴性:如果某个位为 0,则元素一定不在集合中;若元素真的插入过,它的 k 个位置一定都置 1,查询不可能返回“不在”。因此它可以作为安全的“不存在过滤器”,漏报是绝不可能的。

为何常见:概率结构容易让人联想到“猜测”,但这里的概率只向“误报”单方向倾斜。

误区多盖几个章肯定查得更严,k 越大误判率越低,所以 k 越大越好。

正解k 过大会让位数组中的 1 更密集,插入 n 个元素后几乎所有位都变成 1,新元素的 k 个位置很容易全为 1,误判率反而上升。最优 k 由 m 和 n 决定,约等于 (m/n)ln2,在这个值附近误判率最低。

为何常见:直觉上“更多检查等于更严格”,但忽略了多个检查会污染同一个有限空间。

误区布隆过滤器空间小,那应该是把每个元素压缩后存了进去,应该能取出原元素吧?

正解布隆过滤器只保存“某些位被置 1”的痕迹,不保存元素本身,也不能枚举集合中的所有元素。它只能回答“某个元素是否可能在集合中”,若还需要取回元素或统计数量,必须另用存储结构。

为何常见:混淆“判断存在”与“存储内容”,因为普通哈希表两者都能做。

面试题

Q1. 布隆过滤器不存在以下哪种错误?

单选题 · easy

  • A.假阴性(漏判存在元素)← 正确答案
  • B.假阳性(误判不存在元素)
  • C.哈希冲突
  • D.内存不足

答案:A。布隆过滤器的假阴性为零,绝不漏判真正存在过的元素;它只可能把不存在的元素误判为存在,也就是假阳性。A 描述的“漏判存在元素”这种错误在布隆过滤器中不存在。

Q2. 向一个空布隆过滤器插入元素 x 后,会发生什么?

单选题 · easy

  • A.所有位都置为 1
  • B.只有 x 对应的 k 个位置被置为 1← 正确答案
  • C.只有第一个哈希位置被置为 1
  • D.位数组被清空

答案:B。插入元素时,只将它的 k 个哈希函数对应的位置置为 1,不会设置其他位。所有位置 1 或清空都不符合布隆过滤器的插入语义。

Q3. 布隆过滤器查询结果为“不存在”时,该元素真实状态是什么?

单选题 · easy

  • A.一定不存在← 正确答案
  • B.一定存在
  • C.可能存在
  • D.无法判断

答案:A。查询时只要有任何一个哈希位为 0,就说明元素从未被插入过,因此“不存在”的结果是绝对正确的。这就是布隆过滤器零假阴性特性的体现。

Q4. 其他参数不变,位数组长度 m 增大时,误判率会如何变化?

单选题 · mid

  • A.单调下降← 正确答案
  • B.单调上升
  • C.保持不变
  • D.先升后降

答案:A。m 增大使位数组更稀疏,1 不容易密集分布,新元素的 k 个位置同时为 1 的概率降低,因此误判率下降。这与登记板格子越多、章越不容易重叠是同一个道理。

Q5. 经典布隆过滤器不能删除元素的最根本原因是什么?

单选题 · mid

  • A.哈希函数计算太慢
  • B.一个位可能被多个元素共享← 正确答案
  • C.位数组只能用一次
  • D.查询时不支持删除接口

答案:B。多个元素可能映射到同一个位,直接清零会破坏其他元素的查询结果。计数布隆过滤器用计数器记录共享次数,本质上是给每个位增加“引用计数”,才使删除成为可能。

Q6. 给定元素数量 n 和位数组长度 m,最优哈希函数个数 k 的公式是?

单选题 · mid

  • A.(m/n)ln2← 正确答案
  • B.(n/m)ln2
  • C.(ln2)/m
  • D.m/n

答案:A。最优 k 约为 m 除以 n 再乘 ln2,此时位数组中约一半为 1,误判率最低。其他选项要么分子分母颠倒,要么缺少 ln2 系数。

Q7. 当哈希函数个数 k 大到接近位数组长度 m 时,布隆过滤器误判率会怎样?

单选题 · hard

  • A.降为 0
  • B.趋近 1← 正确答案
  • C.稳定在 0.5
  • D.不受影响

答案:B。k 过大时,每次插入几乎都会把大量位置为 1,位数组迅速被填满。到后期查询任何元素时,k 个位置都极可能已是 1,误判率趋近 1,相当于“无论谁来都放行”。

Q8. 以下属于布隆过滤器典型应用场景的有哪些?

多选题 · mid

  • A.缓存穿透拦截← 正确答案
  • B.爬虫 URL 去重← 正确答案
  • C.对字符串精确排序
  • D.压缩存储图片文件

答案:AB。缓存穿透拦截和 URL 去重都只需要判断“是否存在”,且允许少量误判,非常适合布隆过滤器。精确排序需要完整数据,图片压缩需要无损存储,布隆过滤器无法胜任。

Q9. 关于计数布隆过滤器,下列说法正确的是?

多选题 · hard

  • A.支持删除操作← 正确答案
  • B.每位扩展为计数器← 正确答案
  • C.内存占用和经典布隆过滤器相同
  • D.计数器可能溢出← 正确答案

答案:ABD。计数版把位数组的每一位换成计数器,插入加一、删除减一,因此支持删除,但内存占用会增大到原来的数倍;计数器位宽有限,同一个格子计数过多时会发生溢出,需要合理设计。

Q10. 实际元素数量 n 超过设计预期时,下列描述正确的有哪些?

多选题 · extreme

  • A.误判率会高于预期← 正确答案
  • B.误判率会低于预期
  • C.位数组中的 1 会越来越密集← 正确答案
  • D.假阴性仍然保持为 0← 正确答案

答案:ACD。n 超出设计预期会使位数组更拥挤,误判率上升,所以 A 正确、B 错误;位数组中的 1 越来越密集也正确。但只要元素在插入时被正确置位,查询它时 k 个位置一定都是 1,假阴性仍为 0。