Misra-Gries Algorithm——Data Stream Algorithm 学习笔记(一)
因为研究生课程学习到了这份讲义,故在此记录学习笔记,供大家一起讨论。
前言
首先说明本书研究的问题、算法目标、如何评估算法效果。
研究场景
本书的场景是大规模流输入的问题。假设输入是大小为
m
m
m的序列,序列中的每个元素是来自于
1
1
1到
n
n
n的集合。形式化定义如下:
σ
=
⟨
a
1
,
a
2
,
…
,
a
m
⟩
\sigma = \langle a_1, a_2, \dots, a_m\rangle
σ=⟨a1,a2,…,am⟩
a
i
∈
[
n
]
=
{
1
,
2
,
…
,
n
}
a_i\in[n] = \{1, 2, \dots, n\}
ai∈[n]={1,2,…,n}.
以下的讨论都用
m
m
m表示输入序列的长度,
n
n
n表示输入流中元素的范围。
需要研究的问题是,当输入 m m m和 n n n非常庞大,计算机难以存储时,如何用尽量小的空间 s s s来处理大规模输入流。我们希望 s s s至少是亚线性的,即: s = o ( m i n ( m , n ) ) s = o({\rm min}(m, n)) s=o(min(m,n))。最理想的状况是能达到对数空间大小,即 s = O ( log m + log n ) s = O(\log m + \log n) s=O(logm+logn)。通常的算法中,我们能够达到对数多项式空间大小,即 s = p o l y l o g ( min ( m , n ) ) s = {\rm polylog}(\min (m, n)) s=polylog(min(m,n))。其中 f ( n ) = p o l y l o g ( g ( n ) ) f(n) = {\rm polylog}(g(n)) f(n)=polylog(g(n))是指,存在常数 c > 0 c > 0 c>0,使得 f ( n ) = O ( log ( g ( n ) ) c ) f(n) = O(\log (g(n))^c) f(n)=O(log(g(n))c)。
评估算法效果
定义 ϕ ( σ ) \phi(\sigma) ϕ(σ)为问题的真实值,由于真实值一般较难在亚线性空间中得到,我们转而寻找 ϕ ( σ ) \phi(\sigma) ϕ(σ)的近似值,记为 A ( σ ) \mathcal{A}(\sigma) A(σ)。我们希望 A ( σ ) \mathcal{A}(\sigma) A(σ)和 ϕ ( σ ) \phi(\sigma) ϕ(σ)尽可能接近,因此有下列定义。
称满足如下条件的
A
(
σ
)
\mathcal{A}(\sigma)
A(σ)为
(
ε
,
δ
)
(\varepsilon, \delta)
(ε,δ)-estimates:
Pr
[
∣
A
(
σ
)
ϕ
(
σ
)
∣
−
1
]
>
ε
≤
δ
\Pr [\lvert\frac{\mathcal{A}(\sigma)}{\phi(\sigma)}\rvert-1] > \varepsilon \leq\delta
Pr[∣ϕ(σ)A(σ)∣−1]>ε≤δ
在上式中,如果
ϕ
(
σ
)
\phi(\sigma)
ϕ(σ)非常小,接近于零的时候,这个限制条件就会变得过于强了。因此定义满足如下条件的
A
(
σ
)
\mathcal{A}(\sigma)
A(σ)为
(
ε
,
δ
)
+
(\varepsilon, \delta)^+
(ε,δ)+-estimates:
Pr
[
∣
A
(
σ
)
−
ϕ
(
σ
)
∣
>
ε
]
≤
δ
\Pr[\lvert\mathcal{A}(\sigma) - \phi(\sigma)\rvert > \varepsilon]\leq \delta
Pr[∣A(σ)−ϕ(σ)∣>ε]≤δ
频率向量
通常,输入流也可以看作是多重集合,其中的元素可以重复出现。因此,常用频率向量
f
=
(
f
1
,
f
2
,
…
,
f
n
)
\bm{f} = (f_1, f_2, \dots, f_n)
f=(f1,f2,…,fn)来表达
σ
\sigma
σ的特征,其中
f
i
f_i
fi表示
i
i
i这个元素在输入流中出现的频数。
f
i
=
∣
{
j
:
a
j
=
i
}
∣
=
o
c
c
u
r
e
n
c
e
o
f
i
i
n
σ
f_i = \lvert{\{j:a_j = i\}}\rvert={\rm occurence\ of \ }i {\rm \ in\ }\sigma
fi=∣{j:aj=i}∣=occurence of i in σ
也就是说,每个输入流
σ
\sigma
σ都能定义一个
f
\bm{f}
f。同时,
σ
\sigma
σ的每个输入都会在
f
i
f_i
fi上加
1
1
1,因此
σ
\sigma
σ也可以看作是对
f
\bm{f}
f的更新,且所有频数相加应等于
σ
\sigma
σ的长度
m
m
m。
∥
f
∥
1
=
∣
f
1
∣
+
⋯
+
∣
f
n
∣
=
m
\Vert\bm{f}\Vert_1 = \lvert f_1\rvert + \dots + \lvert f_n\rvert =m
∥f∥1=∣f1∣+⋯+∣fn∣=m
频繁项问题
频繁项就是数据流中出现频率最高的项。第一章中将介绍解决该问题的Misra-gries算法,并证明算法的正确性。
问题定义
回到我们前言中定义的问题场景,我们有一个数据流 σ = ⟨ a 1 , a 2 , … , a m ⟩ \sigma = \langle a_1, a_2, \dots, a_m\rangle σ=⟨a1,a2,…,am⟩,其中每个元素 a i ∈ [ n ] = { 1 , 2 , … , n } a_i\in[n] = \{1, 2, \dots, n\} ai∈[n]={1,2,…,n}. σ \sigma σ定义了一个频率向量 f = ( f 1 , f 2 , … , f n ) \bm{f} = (f_1, f_2, \dots, f_n) f=(f1,f2,…,fn),其中 f 1 + f 2 + ⋯ + f n = m f_1 + f_2 + \dots + f_n = m f1+f2+⋯+fn=m.
在学习数据结构的时候,我们都遇到过Majority问题,即要求找到序列中出现频率大于 m 2 \frac{m}{2} 2m的元素,如果有则输出该元素,没有则输出 ⊥ \bot ⊥.
这个问题可以延伸到Frequent问题,即给定 k k k,输出出现频数超过 m k \frac{m}{k} km的元素 { j : f j > m k } \{j:f_j > \frac{m}{k}\} {j:fj>km}.
接下来介绍的Misra-gries算法,可以在输入流一次通过时解决Frequent问题,找到所有出现频数满足要求的元素。而输入流第二次通过时,可以进一步输出这些元素的频数 f i f_i fi。Misra-gries算法实际上是估计元素频数的算法,在输入流一次通过时能够计算 f a ^ \hat{f_a} fa^, f a ^ \hat{f_a} fa^是对 f a f_a fa的估计。
Misra-gries算法
Misra-gries算法可以通过设置参数
k
k
k,保存一个
k
−
1
k-1
k−1大小的key-value数组,数组中始终保持当前频率最高的
k
−
1
k-1
k−1个元素和对应的
f
a
^
\hat{f_a}
fa^。伪代码如下:
其中,line2-3表示,当
j
j
j已经存在于数组中时,让对应频数
A
[
j
]
A[j]
A[j]加
1
1
1。line4-6表示,若数组中不存在元素
j
j
j,且数组中不足
k
−
1
k-1
k−1个元素时,把
j
j
j加入数组,频数设为
1
1
1。line7-9表示,若数组中不存在元素
j
j
j,且有
k
−
1
k-1
k−1个元素时,让所有元素的频数减一,若频数等于零,则移出数组。
Misra-gries算法分析
空间复杂度
如果用平衡二叉树存储key-value数组,其中key,也就是元素的值可以从 [ n ] [n] [n]中取,因此长度为 ⌈ log n ⌉ \lceil \log n\rceil ⌈logn⌉。value,也就是频数,最大不超过 m m m,因此长度为 ⌈ log n ⌉ \lceil \log n\rceil ⌈logn⌉。最多存储 k − 1 k-1 k−1个这样的key-value对。因此空间复杂度为 O ( k ( log m + log n ) ) O(k(\log m + \log n)) O(k(logm+logn))。
正确性分析
我们首先考虑一个和Misra-gries非常相近的算法。主要的区别是,这个算法的数组不仅记录元素对应的频数,还记录每次出现的位置,记为
B
[
j
]
B[j]
B[j]。因此,原本算法中
A
[
j
]
+
1
A[j] + 1
A[j]+1的操作就变为
B
[
j
]
∪
i
B[j]\cup i
B[j]∪i。而
A
[
j
]
−
1
A[j] - 1
A[j]−1的操作则变为从
B
[
j
]
B[j]
B[j]中删除最早的位置。此外,为了方便说明,我们将数列更改为
k
k
k大小。算法伪代码如下:
当然,这样把所有位置都记录下来是非常耗费空间的,这只是用于检验Misra-gries算法效果。
由于 A [ j ] A[j] A[j]表示元素出现的频数, B [ j ] B[j] B[j]表示元素出现的位置集合,所以有 A [ j ] = ∣ B [ j ] ∣ A[j] = \lvert B[j]\rvert A[j]=∣B[j]∣。且 A [ j ] A[j] A[j]中记录的频数 f a ^ \hat{f_a} fa^应小于等于元素真实出现的频数 f a f_a fa, f a ^ ≤ f a \hat{f_a}\leq f_a fa^≤fa。
输入流的位置不可能重复。因此每次在
B
B
B中删去位置时,必然删去了
k
k
k个不同的位置。由于总位置最多
m
m
m个,最多进行
m
k
\frac{m}{k}
km轮删除。由于每次删除对任一元素只删除一个位置,因此
f
a
^
≥
f
a
−
m
k
\hat{f_a}\geq f_a - \frac{m}{k}
fa^≥fa−km,则有:
f
j
−
m
k
≤
f
j
^
≤
f
j
f_j - \frac{m}{k}\leq\hat{f_j}\leq f_j
fj−km≤fj^≤fj
也就是说,只要 f j > m k f_j >\frac{m}{k} fj>km,则最终 A [ j ] A[j] A[j]必然为正, j j j将会保存在数组中。
算法延伸
由算法分析可知,Misra-gries算法可以在输入流一次输入后,找到出现频数超过 m k \frac{m}{k} km的元素。如果要找到这些元素准确的频数 f a f_a fa,只需要再经过一遍输入流,用 k k k大小的数组记录每个原色的频数即可。