问题
如果我们有1个billion的用户名,如何快速的查询某一个用户名是否已经存在呢?
解决方案
假如我们可以考虑使用工具来解决我们的问题,我们可以考虑使用数据库或者cache(比如Redis)来解决这个问题。在本文中,我们不对类似的使用某种服务或者工具的解决方案进行讨论。我们专注于两个基于算法的方案的讨论。
方案一
我们可以考虑使用prefix tree来解决这个问题。prefix tree又叫trie,或者字典树。简单来说,prefix tree使用字符串的prefix构建树。从树根到某一个节点的路径既是某一个字符串的前缀。例如下图所示即为一棵prefix tree:
如果我们将所有的名字构建成一棵prefix tree,我们就可以快速查出某一个名字否是已经存在。另一种和prefix tree可以比较的解决方案是hash table。关于prefix tree和与hash table的比较可以参看这篇文章。
方案二
另一种方案是bloom filter。
Bloom Filter (布隆过滤器) 是一种高效的流带过滤算法。它能迅速判断一个元素是否在集合中。基本特点如下:
-
能够 快速过滤 大量元素,不需要保存整个元素集合。
-
它存在 一定的询问错误率 (偏向 偏积 错误),即可能报错元素在集合中,但是不会报错一个实际在集合中的元素为不在。
-
低内存占用,为大规模流带分析提供优秀解决方案。
Bloom Filter 的工作原理
Bloom Filter 使用 一个字节串 (通常是一个大型数组) 来表示集合,并且使用 k 个哈希算法函数 对每个元素进行对应编码。基本步骤如下:
1. 添加元素
假设我们要将一个元素 X
添加到 Bloom Filter:
-
将
X
通过 k 个不同的哈希函数 计算出 k 个位置 (bit 位); -
将该 k 个位置在字节串中设置为 1;
-
完成添加。
2. 查询元素
要判断一个元素 Y
是否在 Bloom Filter 中:
-
将
Y
通过 k 个不同的哈希函数 计算出 k 个位置; -
如果该 k 个位置都是 1,则该元素 可能在 集合中;
-
如果任意一个位置为 0,则该元素 确实不在 集合中。
如下图所示是一个使用了3个hash函数的bloom filter:
假如我们需要判断“red”是否已经存在,我们就计算3个hash函数的值并查询所对应的位置的bit是否已经被设置。因为3个bit都被设置了,我们可以“red”非常有可能已经存在。
思考
首先我们会发现prefix tree的解决方案看起来更好。首先,该方案更为直接和易于理解。其次,该方案可以给出准确的结果。相反的,bloom filter的解决方案是基于概率的。这样决定了bloom filter的方案只能应用于对于false positive不那么敏感的情况中。这里我们可以举两个例子。例子一,假如我们想在用户创建用户名之前检查该名字是否已经被占用,我们就可以使用bloom filter,因为即便我们错误的将一个不存在的名字判定为已经存在,对用户的影响是极小的,该用户只需要稍微修改自己的用户名即可。例子二,假如我们是一个航空的用户管理系统。我们需要判断一个用户是否是VIP来给予某种优惠服务。那么对于false positive可能就无法接受,因为这意味着我们会错误的给与非VIP用户过多的优惠。
那么为什么还会有bloom filter这样的算法存在?答案是空间的代价。如果我们考虑使用prefix tree处理一个billion大小的字符串集合,我们大概需要500G的内存空间;而bloom filter只需要大概1G的内存空间。
所以我们可以简单的总结如下:如果对于positive false敏感,bloom filter不适用。否则如果数据集较小,可以优先考虑使用prefix tree;如果数据集较大,优先考虑使用bloom filter。