KMP算法
在朴素字符串匹配算法中如果某次匹配失败,则每次把字符串的指针回溯到最开头,这造成了效率低的后果。KMP算法是利用要匹配的模板字符串自身的信息来移动模板字符串的指针。
![]()
图1
如图1所示,在ababa已经匹配的情况下,下一字符不匹配的时候,发现aba是ababa的真后缀同时也是其前缀,同时它也是最长的。由于有一部分后缀等于前缀了,所以不用将要匹配的模板字符的指针移至最起始位置。而要与模板字符匹配必须首先要与其前缀匹配,所以匹配的文本指针不需要移动,只需要继续对比后续的字符。
设要匹配的模板字符为
P
P
P ,设前缀函数
π
\pi
π 求某一字符串其真后缀等于其前缀的最长长度。同时设
π
(
q
)
=
m
a
x
{
k
:
k
<
q
且
P
k
⊐
P
q
}
\pi(q) = max \lbrace k : k<q 且 P_k \sqsupset P_q \rbrace
π(q)=max{k:k<q且Pk⊐Pq} ,即求P的q长度前缀中的其真后缀等于其前缀的最长长度。
求得了
π
(
q
)
\pi(q)
π(q) 函数,可以按照自动机的模式,设状态为q,每次输入更新状态,当状态为模板长度时,就求得了满足的子串。
void KMP_MATCHER(char* T, char* P)
{
int q = 0, i;
for (i = 0; T[i]; i++)
{
while (q > 0 && P[q] != T[i])
q = PI[q];
if (P[q] == T[i])
q++;
if (q == P_LEN)
{
//do something
q = PI[q];
}
}
}
以上代码中,T为搜寻的文本字符串,P为模式字符串,PI为
π
(
q
)
\pi(q)
π(q) 函数。P_LEN为模式P的长度,也是自动机的终态。算法为只要发生不匹配的情况,则移动模式字符串的指针,直到匹配或者已经移到模式字符串的首部了。匹配的话,则匹配的字符数加一,也即是后缀等于前缀的个数加一。
现在的问题转变为如何快速的求解出
π
(
q
)
\pi(q)
π(q) 函数。
计算 π ( q ) \pi(q) π(q) 函数
在计算 π ( q ) \pi(q) π(q) 函数之前,先看几个引理,引理的详细证明可参考《算法导论》。
设
π
∗
(
q
)
=
{
π
[
q
]
,
π
(
2
)
[
q
]
,
.
.
.
,
π
(
t
)
[
q
]
}
\pi^*(q)=\lbrace \pi[q], \pi^{(2)}[q],...,\pi^{(t)}[q]\rbrace
π∗(q)={π[q],π(2)[q],...,π(t)[q]},其中
π
(
i
)
[
q
]
=
π
[
π
(
i
−
1
)
[
q
]
]
\pi^{(i)}[q]=\pi[\pi^{(i-1)}[q]]
π(i)[q]=π[π(i−1)[q]]。迭代至
π
(
i
)
[
q
]
=
0
\pi^{(i)}[q] =0
π(i)[q]=0 时结束。
引理1 设
P
P
P 是长度为
m
m
m 的模式,其前缀函数为
π
\pi
π,对
q
=
1
,
2
,
.
.
.
,
m
q=1,2,...,m
q=1,2,...,m有
π
∗
[
q
]
=
{
k
:
k
<
q
且
P
k
⊐
P
q
}
\pi^*[q]=\lbrace k:k<q 且 P_k \sqsupset P_q \rbrace
π∗[q]={k:k<q且Pk⊐Pq}。
对这条引理可以这么理解,设 r = π ( q ) r=\pi(q) r=π(q) 按照定义 r r r 是 π ∗ ( q ) \pi^*(q) π∗(q) 中最大的值,则次大的值只能在前缀为 r r r 长度,后缀为 r r r 长度里继续匹配,由于后缀已经和前缀相等了,前缀与后缀的匹配变成了与 P r P_r Pr 自身的匹配,变成了子问题,即 π [ r ] \pi[r] π[r] ,因为 π [ r ] = π [ π [ q ] ] \pi[r]=\pi[\pi[q]] π[r]=π[π[q]],以此类推,求得的就是 π ∗ [ q ] \pi^*[q] π∗[q] 。
引理2 设 P P P 是长度为 m m m 的模式,其前缀函数为 π \pi π,对 q = 1 , 2 , . . . , m q=1,2,...,m q=1,2,...,m,如果 π [ q ] > 0 \pi[q]>0 π[q]>0,则 π [ q ] − 1 ∈ π ∗ [ q − 1 ] \pi[q]-1 \in \pi^*[q-1] π[q]−1∈π∗[q−1]。
这条引理比较容易理解,就是匹配的前缀长度减少了一,因为原来前缀就和后缀相同,这时后缀范围减一也自然成立。
这条引理想要说明的一点就是如果
r
∈
π
∗
[
q
−
1
]
r \in \pi^*[q-1]
r∈π∗[q−1] ,那么可能
π
[
q
]
=
r
+
1
\pi[q] = r+1
π[q]=r+1,注意这里仅是可能。我们可以首先把
π
∗
[
q
−
1
]
\pi^*[q-1]
π∗[q−1] 中满足
π
[
q
]
\pi[q]
π[q] 的子集列出来:定义子集
E
q
−
1
=
{
k
∈
π
∗
[
q
−
1
]
:
P
[
k
+
1
]
=
P
[
q
]
}
E_{q-1} =\lbrace k \in \pi^*[q-1]:P[k+1]=P[q] \rbrace
Eq−1={k∈π∗[q−1]:P[k+1]=P[q]}。那么其中
E
q
−
1
E_{q-1}
Eq−1 中的最大值加1即为
π
[
q
]
\pi[q]
π[q]。也就是推论3。
推论3 设
P
P
P 是长度为
m
m
m 的模式,其前缀函数为
π
\pi
π,对
q
=
1
,
2
,
.
.
.
,
m
q=1,2,...,m
q=1,2,...,m,
π
[
q
]
=
{
0
,
如
果
E
q
−
1
=
∅
1
+
m
a
x
{
k
∈
E
q
−
1
}
,
如
果
E
q
−
1
≠
∅
\pi[q] = \begin{cases} 0,&如果E_{q-1}=\emptyset \\ 1+max\lbrace k \in E_{q-1} \rbrace,&如果E_{q-1} \ne \emptyset \end{cases}
π[q]={0,1+max{k∈Eq−1},如果Eq−1=∅如果Eq−1̸=∅
按照最后的推论3来求 π [ q ] \pi[q] π[q] ,再求 E q E_q Eq 。由于 π [ q ] < q \pi[q] < q π[q]<q ,所以先满足条件的肯定是最大的,从而求得 π [ q ] \pi[q] π[q]。
void COMPUTE_PREFIX_FUNCTION(char* P)
{
int q, k;
P_LEN = strlen(P);
PI[1] = 0;
k = 0;
for (q = 2; q <= P_LEN; q++)
{
while (q > 0 && P[k] != P[q-1])
k = PI[k];
if (P[k] == P[q - 1])
k++;
PI[q] = k;
}
}
参考
算法导论