KMP算法简介
KMP算法是一种字符串匹配算法,算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。时间复杂度 为 O ( m + n ) O(m+n) O(m+n),其中 n n n 为主串长度, m m m 为模式串长度。
暴力算法匹配字符串
用暴力算法匹配字符串过程中,我们会把 T [ 0 ] T[0] T[0] 跟 W [ 0 ] W[0] W[0] 匹配,如果相同则匹配下一个字符,直到出现不相同的情况,此时我们会丢弃前面的匹配信息,然后把 T [ 1 ] T[1] T[1] 跟 W [ 0 ] W[0] W[0] 匹配,循环进行,直到主串结束,或者出现匹配成功的情况。这种丢弃前面的匹配信息的方法,极大地降低了匹配效率。这种匹配方式的 时间复杂度为: O ( m ∗ n ) O(m*n) O(m∗n)。示意图如下所示:
KMP算法
KMP算法的改进之处在于:每一趟匹配过程中出现字符不等时,不需要回溯 i i i 指针,而是利用已经得到的 “部分匹配” 的结果将模式串向右 “滑动” 尽可能远的一段距离,继续进行比较。如下图所示:
- 在第一趟匹配中, p i = 3 ≠ s j = 3 p_{i=3} \neq s_{j=3} pi=3=sj=3;
- 在第二趟中,保持 i = 3 i=3 i=3 不变,模式串向右滑动至 j = 1 j=1 j=1,再进行匹配,在匹配的过程中发现 p i = 7 ≠ s j = 5 p_{i=7} \neq s_{j=5} pi=7=sj=5;
- 在第三趟匹配过程中,保持 i = 7 i = 7 i=7 不变,模式串向右滑动至 j = 2 j = 2 j=2,在进行匹配 …
在整个滑动的过程中,主串的标签 i i i 始终是向前滑动的,不会回溯。
现在讨论一般情况,假设模式串为 " p 1 p 2 p 3 . . . p m " " p_1p_2p_3...p_m " "p1p2p3...pm",主串为 " s 1 s 2 s 3 . . . s n " " s_1s_2s_3...s_n " "s1s2s3...sn",从上例分析可知,当在匹配过程中产生 “失配” (即 p j ≠ s i p_j \neq s_i pj=si)时,模式串应该 “向右滑动” 到什么位置,即当主串中 i i i 个字符与模式中第 j j j 个字符 “失配” 时,主串中的第 i i i 个字符应该与模式串中哪个字符匹配?
假设此时应与模式串第
k
k
k 个字符进行比较(
k
<
j
k < j
k<j),则模式串中前
k
−
1
k-1
k−1 个字符的子串和主串比满足以下关系:
"
p
1
p
2
.
.
.
p
k
−
1
"
=
"
s
i
−
k
+
1
s
i
−
k
+
2
.
.
.
s
i
−
1
"
" p_1p_2...p_{k-1} " = " s_{i-k+1}s_{i-k+2}...s_{i-1} "
"p1p2...pk−1"="si−k+1si−k+2...si−1"
而原先部分匹配的结果为:
"
p
j
−
k
+
1
p
j
−
k
+
2
.
.
.
p
j
−
1
"
=
"
s
i
−
k
+
1
s
i
−
k
+
2
.
.
.
s
i
−
1
"
" p_{j-k+1}p_{j-k+2}...p_{j-1} " = " s_{i-k+1}s_{i-k+2}...s_{i-1} "
"pj−k+1pj−k+2...pj−1"="si−k+1si−k+2...si−1"
有上述两式可以得到如下结果:
"
p
1
p
2
.
.
.
p
k
−
1
"
=
"
p
j
−
k
+
1
p
j
−
k
+
2
.
.
.
p
j
−
1
"
" p_1p_2...p_{k-1} " = " p_{j-k+1}p_{j-k+2}...p_{j-1} "
"p1p2...pk−1"="pj−k+1pj−k+2...pj−1"
上式中
p
1
p
2
.
.
.
p
k
−
1
p_1p_2...p_{k-1}
p1p2...pk−1 是已经匹配的子串(
p
1
p
2
.
.
.
p
j
−
2
p
j
−
1
p_1p_2...p_{j-2}p_{j-1}
p1p2...pj−2pj−1)中出现的 最大的重叠前缀和后缀。
若令
n
e
x
t
[
j
]
=
k
next[j] = k
next[j]=k,则
n
e
x
t
[
j
]
next[j]
next[j] 表明模式串中第
j
j
j 字串与主串相应的字符 “失配” 时,模式串应该向右 “滑动” 至第
k
k
k 个字符。
n
e
x
t
[
j
]
next[j]
next[j] 的函数定义可以如下:
n e x t [ j ] = { 0 ; j = 1 ( 第 一 个 字 符 就 不 匹 配 : 主 串 、 模 式 串 要 同 时 移 动 ) k ; 重 叠 k − 1 的 字 符 1 ; 不 存 在 重 叠 字 符 next[j]= \left\{\begin{matrix} 0; & j=1(第一个字符就不匹配:主串、模式串要同时移动) \\ k; & 重叠 k-1 的字符\\ 1; & 不存在重叠字符 \end{matrix}\right. next[j]=⎩⎨⎧0;k;1;j=1(第一个字符就不匹配:主串、模式串要同时移动)重叠k−1的字符不存在重叠字符
由上述定义可以推出模式串的 n e x t next next 函数值如下:
j | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
模式串 | a | b | a | a | b | c | a | c |
n e x t [ j ] next[j] next[j] | 0 | 1 | 1 | 2 | 2 | 3 | 1 | 2 |
利用 n e x t [ j ] next[j] next[j] 函数进行移动的过程如下:
- 在匹配过程中产生 “失配” 时,指针 i i i 不变,指针 j j j 回退到 n e x t [ j ] next[j] next[j] 所指示的位置上重新比较;
- 并且当 j j j 退至零时,指针 i i i 和指针 j j j 需同时增 1 1 1,即若主串的第 i i i 个字符和模式的第 1 1 1 个字符不等时,应从主串的第 i + 1 i+1 i+1 个字符重新进行匹配。
匹配代码如下:
int Index_KMP(char *s, char *p, int pos) {
// 利用模式串 p 的next函数在主串 s 中第 pos 个字符之后的位置
// 进行kmp匹配,其中 1 <= pos <= strlen(p)
// 字符起始位置从 1 开始,s[0],p[0]存储字符串的个数
// 返回匹配的起始位置
int i = pos, j = 1;
while(i <= s[0] && j <= p[0]) {
if (j == 0 || s[i] == p[j]) { // 继续比较后续字符
++i; ++j;
}
else j = next[j]; // 模式串向后滑动
}
if (j > p[0]) return i - s[0]; // 匹配成功
else return 0; // 匹配失败
}
上述算法的时间复杂度为: O ( n ) O(n) O(n), n n n 为主串的长度。
求解 n e x t [ j ] next[j] next[j] 函数
KMP 算法是在已知
n
e
x
t
next
next 函数值的基础之上执行的,那么,如何求得模式串的
n
e
x
t
[
j
]
next[j]
next[j] 函数值呢?由上述讨论可知函数
n
e
x
t
[
j
]
next[j]
next[j] 只与模式串自身有关。
有定义可知:
n
e
x
t
[
1
]
=
0
next[1] = 0
next[1]=0;
如果
n
e
x
t
[
j
]
=
k
next[j] = k
next[j]=k,则存在匹配情况(前缀和后缀的匹配
k
−
1
k-1
k−1 个字符):
p
1
.
.
.
p
k
−
1
=
p
j
−
k
+
1
.
.
.
p
j
−
1
p_1...p_{k-1} = p_{j-k+1}...p_{j-1}
p1...pk−1=pj−k+1...pj−1
那么
n
e
x
t
[
j
+
1
]
=
?
next[j+1] = ?
next[j+1]=?
如果:
p
k
=
p
j
p_k = p_j
pk=pj,则存在子串(前缀后缀有
k
k
k字符匹配)
p
1
.
.
.
p
k
−
1
p
k
=
p
j
−
k
+
1
.
.
.
p
j
−
1
p
j
.
p_1...p_{k-1}p_k = p_{j-k+1}...p_{j-1}p_j.
p1...pk−1pk=pj−k+1...pj−1pj.
那么有表达式:
n
e
x
t
[
j
+
1
]
=
k
+
1
=
n
e
x
t
[
j
]
+
1.
next[j+1] = k + 1 = next[j] + 1.
next[j+1]=k+1=next[j]+1.
如果
p
k
≠
p
j
p_k \neq p_j
pk=pj,则存在
p
1
.
.
.
p
k
−
1
p
k
≠
p
j
−
k
+
1
.
.
.
p
j
−
1
p
j
.
p_1...p_{k-1}p_k \neq p_{j-k+1}...p_{j-1}p_j.
p1...pk−1pk=pj−k+1...pj−1pj. 我们现在把
p
1
.
.
.
p
k
−
1
p
k
p_1...p_{k-1}p_k
p1...pk−1pk 当作模式串,以匹配主串
p
j
−
k
+
1
.
.
.
p
j
−
1
p
j
p_{j-k+1}...p_{j-1}p_j
pj−k+1...pj−1pj,那么模式串应该以
k
=
n
e
x
t
[
k
]
k = next[k]
k=next[k]的方式向右滑动,直至出现
k
k
k 值使得:
p
1
.
.
.
p
k
−
1
p
k
=
p
j
−
k
+
1
.
.
.
p
j
−
1
p
j
p_1...p_{k-1}p_{k} = p_{j-k+1}...p_{j-1}p_j
p1...pk−1pk=pj−k+1...pj−1pj。那么就存在表达式:
n
e
x
t
[
j
+
1
]
=
k
+
1
next[j+1] = k + 1
next[j+1]=k+1
如下图例子所示:
j | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
模式串 | a | b | a | a | b | c | a | c |
n e x t [ j ] next[j] next[j] | 0 | 1 | 1 | 2 | 2 | 3 | 1 | 2 |
void get_next(char *p, int next[]) {
// 求模式串 s 的 next 函数值并存入数组 next 中
int j = 1, k=0;
next[1] = 0; // 表示模式串和子串都要向右滑1 位
while(j <= p[0]) {
if (k == 0 || p[j] == p[k]) { // 计算next[j+1]的值
next[j+1] = k + 1;
++j; ++k;
}
else k = next[k]; // 寻找与s[j]匹配的字符
}
}
上述算法的时间复杂度为: O ( m ) O(m) O(m)。
改进 n e x t [ j ] next[j] next[j] 算法
上述
n
e
x
t
[
j
]
next[j]
next[j] 算法存在一些缺陷。
假如存在模式串 $ p = “aaaab”$ 与 主串
s
=
"
a
a
a
b
a
a
a
a
b
"
s = "aaabaaaab"
s="aaabaaaab",则匹配过程如下:
模式串的 n e x t [ ] next[] next[] 函数为:
j | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
模式串 | a | a | a | a | b |
n e x t [ j ] next[j] next[j] | 0 | 1 | 1 | 1 | 2 |
观察模式串发现,在模式串中第
1
,
2
,
3
1,2,3
1,2,3 个字符和第
4
4
4 个字符都相等,因此不需要再和主串第
4
4
4 个字符相比较,而可以一气呵成向右滑动
4
4
4 个字符,直接进行
i
=
5
,
j
=
1
i=5,j=1
i=5,j=1 的比较。
如存在
n
e
x
t
[
j
]
=
k
next[j] = k
next[j]=k,且
p
j
=
=
p
k
p_j == p_k
pj==pk,正常情况下
p
j
≠
s
i
p_j \neq s_i
pj=si,模式串应该滑动至
k
k
k 的位置,进行比较
p
k
=
?
s
i
p_k =? s_i
pk=?si;由于
p
j
=
=
p
k
p_j == p_k
pj==pk,可想而知
p
k
≠
s
i
p_k \neq s_i
pk=si;所以模式串不应该滑动至
k
k
k 的位置,应该以方法
k
=
n
e
x
t
[
k
]
k = next[k]
k=next[k] 进行滑动直至出现
p
j
≠
p
k
p_j \neq p_k
pj=pk.
void get_next(char *p, int next[]) {
// 求模式串 p 的 next 函数修正值存入数组 nextval 中
int j = 1, k = 0;
next[1] = 0; // 表示模式串和子串都应该向右滑动一位
while(j <= p[0]) {
if (k == 0 || p[j] == p[k]) {
++j; ++k;
if (p[j] != p[k]) next[j] = k;
else next[j] = next[k];
}
else k = next[k]; // 更新k值,直至出现p[j] = p[k]
}
}
以上代码好像有点问题,如果
p
[
j
]
=
=
p
[
n
e
x
t
[
k
]
]
p[j] == p[next[k]]
p[j]==p[next[k]] 呢?
个人觉得最清晰明确的方法就是在初次构成 next
数组之后,再检查一次 next
数组,排除 p[j] == p[k]
的情况,如下所示:
void check_next(char *p, int next[]) {
// next 已经经过初始化
// 排除next数组中 p[j] == p[k] 的情况
int j = 1, k = 0;
next[j] = k;
while(j <= p[0]) {
if(k== 0 || p[j] != p[k]) {
next[j] = k; // 更新 next[j]
j = j + 1; // 检测下一个
k = next[j];
}
else k = next[k]; // 排除p[j] == p[k] 的情况;
}
}