KMP 有什么用
KMP(Knuth,Morris和Pratt这三位发明者的首字母)算法,用于模式匹配(字符串匹配):
给定字符串 S 0 S 1 . . . S n − 1 S_0S_1...S_{n-1} S0S1...Sn−1,和模式串 T 0 T 1 . . . T m − 1 T_0T_1...T_{m-1} T0T1...Tm−1,在字符串 S S S 中寻找模式串 T T T 的过程称为模式匹配。如果匹配成功,返回 T T T 在 S S S 中的位置;否则匹配失败,返回 -1.
朴素模式匹配算法—Brute Force 算法
在介绍 KMP 算法之间,先看一个暴力求解的 Brute Force 算法,基本思想:
- 从字符串 S S S(主串) 的第一个字符开始和模式 T T T 的第一个字符进行比较,若相等,则继续依次比较后续的字符;否则,从字符串 S S S 的第二个字符重新开始和 T T T 的第一个字符进行比较。
- 重复上面的过程,直到 T T T 中字符全都比较完毕,说明匹配成功;或 S S S 中字符全部比较完,说明匹配失败。
int strMatch_BF(string S, string T, int pos=0) {
int i = pos, j = 0;
while(i < S.size() && j < T.size()) {
if(S[i] == T[j]) {
++i;
++j;
} else {
i = i - j + 1; // 从本趟匹配开始位置的下一个字符重新开始匹配。
j = 0; // 模式也从第一个字符开始重新匹配。
}
}
if(j == T.size()) {
return i - T-size(); // 返回本趟匹配的起始位置
} else {
return -1;
}
}
BF 算法的时间性能较低,因为在每趟匹配不成功时存在大量回溯,没有利用已经部分匹配的结果。
那么可不可以在匹配失败时,让主串不回溯?
- 如果主串不回溯,那么就需要模式串向右滑动一段距离。
- 这就是 KMP 算法的主要思想,下面介绍如何确定模式串的滑动距离,也就是前缀表(或next数组)。
KMP 算法主要思想
经过前面的分析,在匹配失败时,我们 不 去 回 溯 主 串 , 而 是 将 模 式 串 尽 可 能 地 向 右 “ 滑 动 ” \color{red}{不去回溯主串,而是将模式串尽可能地向右“滑动”} 不去回溯主串,而是将模式串尽可能地向右“滑动”,这就是 KMP 算法的主要思想。
思考的开始:
- 假定主串 S 0 S 1 . . . S n − 1 S_0S_1...S_{n-1} S0S1...Sn−1,和模式串 T 0 T 1 . . . T m − 1 T_0T_1...T_{m-1} T0T1...Tm−1;
- 无回溯匹配问题变成:当主串的字符 S i S_i Si 与模式中的字符 T j T_j Tj 不匹配时,主串的 S i S_i Si 应与模式中的哪个字符对齐再进行比较呢?
- 这个信息我们将其存在 next 数组,也就是前缀表当中。所以前缀表的作用就是:
前缀表中记录了模式串与主串不匹配的时候,模式串应该从哪里开始重新匹配。
进一步思考:
- 假设主串中的
S
i
S_i
Si 与模式中的
T
k
T_k
Tk(
k
<
j
k < j
k<j)继续比较(即
n
e
x
t
[
j
]
=
k
next[j]=k
next[j]=k),则应有:
- T 0 T 1 . . . T k − 1 = S i − k S i − k + 1 . . . S i − 1 T_0T_1...T_{k-1} = S_{i-k}S_{i-k+1}...S_{i-1} T0T1...Tk−1=Si−kSi−k+1...Si−1;
- 可能有多个 k k k,那么选哪一个?
- 注意这里的 k < j \color{red}{k < j} k<j 是必须的,因为我们要将模式串向后移,如果 k = j k=j k=j 的时候,这不就相当于没移了吗?这是计算前缀表时必须注意的一点。
- 而根据已有的匹配,有:
- T j − k T j − k + 1 . . . T j − 1 = S i − k S i − k + 1 . . . S i − 1 T_{j-k}T_{j-k+1}...T_{j-1}=S_{i-k}S_{i-k+1}...S_{i-1} Tj−kTj−k+1...Tj−1=Si−kSi−k+1...Si−1
- 所以:
- T 0 T 1 . . . T k − 1 = T j − k T j − k + 1 . . . T j − 1 T_0T_1...T_{k-1}=T_{j-k}T_{j-k+1}...T_{j-1} T0T1...Tk−1=Tj−kTj−k+1...Tj−1
- 如果 k < j k < j k<j的话, T j − k T j − k + 1 . . . T j − 1 T_{j-k}T_{j-k+1}...T_{j-1} Tj−kTj−k+1...Tj−1 就不可能是前缀。
那么 T 0 T 1 . . . T k − 1 = T j − k T j − k + 1 . . . T j − 1 T_0T_1...T_{k-1}=T_{j-k}T_{j-k+1}...T_{j-1} T0T1...Tk−1=Tj−kTj−k+1...Tj−1 说明了什么?
- 模式串从第 0 位向右数 k k k 位,和从第 j − 1 j-1 j−1 位向前数 k k k 位,是相同的。
- k k k 与 j j j 具有函数关系,由当前匹配失败的位置 j j j( T j T_j Tj),可以计算出滑动位置 k k k(即新的比较位置 T k T_k Tk)。
- 滑动位置 k k k(新的比较位置 T k T_k Tk)仅与模式串 T T T 自身相关,与主串无关。
k
k
k 必须满足
k
<
j
k<j
k<j,且应该取所有可能值中的最大值,因为取最大值就意味着向右移动的距离最小(可以意会一下),这也就避免错过成功匹配的机会,即:
k
=
m
a
x
{
k
∣
0
<
k
<
j
,
T
0
T
1
.
.
.
T
k
−
1
=
T
j
−
k
T
j
−
k
+
1
.
.
.
T
j
−
1
}
k = max\{k| 0<k<j,T_0T_1...T_{k-1}=T_{j-k}T_{j-k+1}...T_{j-1}\}
k=max{k∣0<k<j,T0T1...Tk−1=Tj−kTj−k+1...Tj−1}
前缀表(next数组)的计算方法
前缀表中存储的就是模式串 T T T 中的每一位 T j T_j Tj 匹配失败时,模式串的哪一位 T k T_k Tk 要和当前主串失配的位置对齐,令 k = n e x t [ j ] k=next[j] k=next[j]:
next [ j ] = { − 1 j = 0 m a x { k ∣ 0 < k < j , T 0 T 1 . . . T k − 1 = T j − k T j − k + 1 . . . T j − 1 } 0 other case \operatorname{next}[j]=\left\{\begin{array}{lc} -1 & j=0 \\ max\{k| 0<k<j,T_0T_1...T_{k-1}=T_{j-k}T_{j-k+1}...T_{j-1}\} \\ 0 & \text { other case } \end{array}\right. next[j]=⎩⎨⎧−1max{k∣0<k<j,T0T1...Tk−1=Tj−kTj−k+1...Tj−1}0j=0 other case
对于模式串的任一位置 j j j, k = n e x t [ j ] k=next[j] k=next[j] 实质上是找模式串的字串 T 0 T 1 . . . T j − 1 T_0T_1...T_{j-1} T0T1...Tj−1中的最长的相同前缀( T 0 T 1 . . . T k − 1 T_0T_1...T_{k-1} T0T1...Tk−1)和后缀( T j − k T j − k + 1 . . . T j − 1 T_{j-k}T_{j-k+1}...T_{j-1} Tj−kTj−k+1...Tj−1)。
n e x t next next 数组的涵义详解:
-
j
=
0
j=0
j=0 时,
n
e
x
t
[
j
]
next[j]
next[j] 初始化为
−
1
-1
−1。
- n e x t [ j ] = − 1 next[j]=-1 next[j]=−1 表示不将当前失配位置 S i S_i Si 和模式串中的字符进行比较,而是把模式头部与主串的下一个字符进行比较。
-
j
>
0
j>0
j>0 时,
n
e
x
t
[
j
]
next[j]
next[j] 的值为:模式串前
j
j
j 的字符构成的字串
T
0
T
1
.
.
.
T
j
−
1
T_0T_1...T_{j-1}
T0T1...Tj−1 中所出现的首尾相同的字串的最大长度。
- 模式从 n e x t [ j ] next[j] next[j] 位和主串的失配位置进行比较。
- 注意 n e x t [ j ] < j next[j]<j next[j]<j,即 k < j k<j k<j。
- 当
j
>
0
j > 0
j>0 且无首尾相同字串时,
n
e
x
t
[
j
]
=
0
next[j]=0
next[j]=0。
- n e x t [ j ] = 0 next[j]=0 next[j]=0表示,将模式串的 T 0 T_0 T0 与主串失配位置开始进行比较。
void GetNext(string T, vector<int>& next) {
next.resize(T.size());
int j=0, k=-1;
next[0] = -1; // 初始化
while(j < T.size()) {
if(k == -1 || T[j] == T[k]) {
j++; k++;
next[j] = k;
} else {
k = next[k];
}
}
}
算法时间复杂度:O(m)。
说白了,就是从模式串当中的每一个字符 T j T_j Tj之前的 j j j个字符中,找最长的相同前缀和后缀的长度 k k k。
KMP 算法实现
KMP 算法实现步骤:
- 在主串 S S S 和模式 T T T 分别设比较的起始下标 i , j i,j i,j;
- 循环直到
S
S
S 中所剩的字符长度小于
T
T
T 的长度或
T
T
T 中所有字符均比较完毕:
- 如果 S [ i ] = = T [ j ] S[i] == T[j] S[i]==T[j],继续比较 S S S 和 T T T 的下一个字符;
- 否则将模式串“向右滑动”,
j
=
n
e
x
t
[
j
]
j=next[j]
j=next[j],也就是将
k
=
n
e
x
t
[
j
]
k=next[j]
k=next[j] 处字符与当前主串失配位置
S
i
S_i
Si 对齐;
- 如果 j = = 0 j==0 j==0,那么将 i i i 和 j j j 分别加 1,准备下一趟从头开始的比较。
- 这里注意,如果 j = = 0 j==0 j==0,我们先是把 n e x t [ 0 ] = − 1 next[0]=-1 next[0]=−1 给了 j j j, j + + j++ j++ 之后相当于回到了模式串的初始字符。
- 如果 T T T 中所有字符比较完毕,则返回主串中的起始下标;否则返回 0.
int StrMatch_KMP(string S, string T, int pos=0) {
int i=pos, j=0;
while(i < S.size() && j < T.size()) {
if(S[i] == T[j]) {
i++; j++;
} else {
j = next[j];
if(j == 0) { // 模式串的初始字符就匹配失败了
i++; // 此时就要略过主串的失配位置,向后移
j++; // 模式串位置也要从 -1 变成 0
}
}
}
if(j == T.size()) {
return i-T.size();
} else {
return -1;
}
}
本文 next 数组的实现和网上其他教程不太一样,但个人认为这种 next 数组的表示形式更容易理解一些,就是将每个位置
T
j
T_j
Tj 之前的
T
0
T
1
.
.
.
T
j
−
1
T_0T_1...T_{j-1}
T0T1...Tj−1 中最长的相同前缀和后缀的长度
k
k
k 存到数组当中,而且注意 1) k < j ;2) next[0]=-1
这两点就好了。