Probabilistic Data Structures
Table of Contents
1. 概率数据结构简介
在“大数据”场景下,一些传统的数据结构很可能由于占用内存太多而不再适用。概率数据结构适应于大数据和流式应用,它们使用的内存很少,不过存在一定的错误率。
在书籍 Probabilistic Data Structures and Algorithms for Big Data Applications 中,总结了一些“概率数据结构”,如下:
- Membership (Bloom filter, Counting Bloom filter, Quotient filter, Cuckoo filter).
- Cardinality (Linear counting, probabilistic counting, LogLog, HyperLogLog, HyperLogLog++).
- Frequency (Majority algorithm, Frequent, Count Sketch, Count-Min Sketch).
- Rank (Random sampling, q-digest, t-digest).
- Similarity (Locality-Sensitive Hashing, MinHash, SimHash).
本文将选取几种典型的数据结构进行描述。
2. Membership
Membership 是指“查询集合中是否存在指定的元素”。
2.1. Bloom Filter
Bloom Filter 是 1970 年由 Burton Howard Bloom 提出来的。它是一种空间效率很高的数据结构,用于检测一个元素是否存在于集合中。
Bloom Filter 的原理为:设计一个长度为
Figure 1: Bloom Filter 原理
图 1 的例子中,我们将元素
当 Bloom Filter 认为一个元素存在于集合中时,这个元素可能并不在集合中。而当 Bloom Filter 认为一个元素不在集合中时,那么这个元素一定不在集合中。
2.1.1. Hash 算法
Bloom Filter 算法经常使用 MurmurHash 和 FNV 作为其 Hash 算法,这类算法追求“运算速度”和“哈希的随机性”,而不在意安全性。
2.1.2. 优缺点
Bloom Filter 优点:
1、空间效率高,所占内存小。
2、查询时间短。
Bloom Filter 缺点:
1、元素添加到集合中后,不能被删除。
2、存在误判。
2.2. Counting Bloom Filter
Bloom Filter 有个缺点:元素添加到集合中后,不能被删除。
Counting Bloom Filter 克服了上述问题,它将标准 Bloom Filter 位数组的每一比特位扩展为一个小的计数器(Counter),在插入元素时给对应的
Counting Bloom Filter 原理如图 2 所示(上一排是 Bloom Filter,下一排是 Counting Bloom Filter)。
Figure 2: Counting Bloom Filter(用 Counter 代替“0/1 比特位”)
2.3. Cuckoo Filter
Cuckoo Filter 于 2014 年提出,它相对于 Bloom Filter 的最大优点是: Cuckoo Filter 支持元素的删除操作。 当然,Cuckoo Filter 还宣称比 Bloom Filter 的错误率更低,内存占用更少。这里不详细介绍它。
3. Cardinality
Cardinality 是指“集合中的不同元素的个数”。
3.1. HyperLogLog
问题:统计网页中的一个链接,每天有多少用户点击(同一个用户的反复点击仅记为 1 次)。
这个问题可以抽象为:如何统计集合中不同元素的个数。当数据量不大时,用 HashMap,Set 等数据结构就能解决;当数据量很大时,内存不足以保证所有元素,这时问题变得麻烦了。如果不要求精确统计,则可以使用下面介绍的 HyperLogLog 算法。
HyperLogLog 原始论文:HyperLogLog: the analysis of a near-optimal cardinality estimation algorithm
3.1.1. 基本思想
考虑下面“抛硬币”实验。硬币有“正反”(后面用 0/1 分别表示正/反面)两面,抛向地面,正反两面出现的概率都是 50%。
只要出现了正面,就记为一次实验。
下面是
1 0 1
连续反面最多出现 1 次,即
下面是
0 1 0 1 1 0 0 1 1
连续反面最多出现 2 次,即
显然,
按照这个公式,当最多有 2 次扔出连续的反面时(即出现一次序列 001 时),我们估计你可能进行了 8 次实验;当最多有 10 次扔出连续的反面时,我们估计你可能进行了 2048 次实验。
求集合不同元素个数和上面实验有什么关系呢? 每个元素
01xxxxxx 01xxxxxx 1xxxxxxx 001xxxxx 01xxxxxx
统计得到,前缀中连续 0 最多出现 2 个,那么我可估计集合中元素个数不会太多。
3.1.2. 减少误差:分桶
前面介绍的做法,其误差比较大,容易受到突发事件(比如突然连续抛出好多 0)的影响,HyperLogLog 算法的重点在于研究如何减小这个误差。
最简单的一种优化方法显然就是把数据分成
对于计算数据流中不同元素个数问题。我们对元素哈希后得到的“随机比特串”,再按下面办法把它拆分为
前两个比特位如果为 00,则对应比特串放入桶 1 中(放入桶前要先去掉前两个比特位 00 );
前两个比特位如果为 01,则对应比特串放入桶 2 中(放入桶前要先去掉前两个比特位 01 );
前两个比特位如果为 10,则对应比特串放入桶 3 中(放入桶前要先去掉前两个比特位 10 );
前两个比特位如果为 11,则对应比特串放入桶 4 中(放入桶前要先去掉前两个比特位 11 )。
显然,不同的元素会位于不同的桶中,每个桶的元素个数大致相等。这 4 个估计值求平均值后,再乘以 4 可以得到一个误差更小的估计值。即集合中不同元素个数为(记为 Distinct Value, DV):
这是 LogLog 算法的计算公式,constant 是修正因子,它的具体值是不定的;
平均数容易受到大的数值的影响, HyperLogLog 采用调和平均数(Harmonic Mean)来替换上面的平均数,得到公式:
上式中,
4. Frequency
Frequency 是指“统计实时的数据流中元素出现的频率(次数)”。
4.1. Count-Min Sketch
如何计算实时的数据流中某元素出现的次数(不要求精确)呢?如果数据流中的元素种类不是很多,则我们可以直接使用 HashMap 来统计,每出现一次元素,把其相应计算增加一即可。不过,如果数据流中的元素种类非常地多,则由于会占用内存太多,我们无法使用 HashMap 了。
如果不需要精确的计算,Count-Min Sketch 可以解决这类问题。
Count-Min Sketch 不存储所有元素的计数,仅存在元素 Hash 值的计数。其思路如下: 创建一个长度为
显然,由于有哈希冲突(不同元素有相同哈希值),所以这种方法得到的元素出现次数会偏大。
为了降低计数误差,我们可以使用多个(如
Figure 3: Count-Min Sketch 演示
注1:Count-Min Sketch 算法只会估算偏大,不会偏小。
注2:Count-Min Sketch 可以认为是 Bloom Filter 用于统计领域的变型。