在主串 S 中定位子串 T 称为串的模式匹配或者串匹配,比较著名的匹配算法有BF算法和KMP算法。
BF算法
BF(Brute-Force)算法的基本思想为:从在主串 S 中指定的pos位置及子串 T 的第一个位置开始:
①如果 S[ i ] == T[ j ],则继续匹配下一个字符,即进行++i、++j
②如果 S[ i ] != T[ j ],则 i 指针回退到 ++pos 位置,j指针回退到 0
int BF(string& S, string& T,int pos=0) {
int m = S.size();
int n = T.size();
int i = pos;
int j = 0;
while (i < m && j < n) {
if (S[i] == T[j]) {
++i;
++j;
}
else {
i = ++pos;
j = 0;
}
}
return j == n ? pos : -1;
}
BF算法的空间复杂度为 O(1),时间复杂度为 O(mn),m 为主串 S 的长度,n 为子串 T 的长度,但在实际中其时间复杂度大多数情况下接近 O(m + n)。
KMP算法
在执行BF算法时,主串的指针 i 在失配时回退导致时间复杂度上升,既然 i 之前的字符串我们已经遍历了,是否可以让指针 i 不回退。
我们这里的下标从 0 开始
由于框起来的3个部分相等,我们发现当S[ 6 ]与T[ 6 ]失配时,i 指针完全可以不用回退,只需将 j 指针回退到 3 即可。
那么当S[ i ]与T[ j ]失配时,j 需要回退到哪个位置呢。假设 j 需要 回退到第 k 个位置,k < j,此时有
t 0 t 1 . . . t k − 1 t_0t_1...t_{k-1} t0t1...tk−1== s i − k . . . s i − 2 s i − 1 s_{i-k}...s_{i-2}s_{i-1} si−k...si−2si−1
而我们又知道 i 指针之前的字符都是匹配的,因此有
t j − k . . . t j − 2 t j − 1 t_{j-k}...t_{j-2}t_{j-1} tj−k...tj−2tj−1== s i − k . . . s i − 2 s i − 1 s_{i-k}...s_{i-2}s_{i-1} si−k...si−2si−1
可以推出k应该满足以下条件:
t 0 t 1 . . . t k − 1 t_0t_1...t_{k-1} t0t1...tk−1== t j − k . . . t j − 2 t j − 1 t_{j-k}...t_{j-2}t_{j-1} tj−k...tj−2tj−1
即如果子串 T 的前 k(从1开始数数) 个字符与子串 T 的 j 指针之前的 k 个字符相匹配,那么只需子串指针 j 回退到 k 位置即可。
我们也发现 j 指针回退到哪个位置至于子串 T 失配的位置 j 相关,与主串 S 无关,考虑使用一个组数 next,next[ j ] 表示当 j 位置失配时指针 j 应该回退到哪个位置。下面考虑 next 数组的特殊情况,当
T[ 0 ]失配时,指针 j 已经无退可退,此时应将主串 S 的指针 i 前进一个,即 i+=1,可以使用next[ j ]=-1标记这种情况。
下面考虑怎么获得 next 数组
假设next[ j - 1] = k,表明子串 T 的的前 k 个字符是与 j-1 指针之前的前 k 个字符是相等的,故我们现在需要考虑T[ k ]与T[ j-1 ]的匹配情况即可:
- 如果T[ k ] == T[ j-1 ]:表明
t
0
t
1
.
.
.
t
k
t_0t_1...t_{k}
t0t1...tk==
t
j
−
k
−
2
.
.
.
t
j
−
2
t
j
−
1
t_{j-k-2}...t_{j-2}t_{j-1}
tj−k−2...tj−2tj−1,
因此next[ j ] = k+1,即next[ j ] = next[ j -1 ]+1 - 如果T[ k ] != T[ j-1 ]:表明
t
0
t
1
.
.
.
t
k
t_0t_1...t_{k}
t0t1...tk!=
t
j
−
k
−
2
.
.
.
t
j
−
2
t
j
−
1
t_{j-k-2}...t_{j-2}t_{j-1}
tj−k−2...tj−2tj−1
我们已知图中①部分是相等的,进而可以得到绿色部分②也是相等的。
令x=next[ next[ j - 1 ] ],如果T[ x ]==T[ j - 1 ],则 j 位置的最大前后缀匹配长度为next[ x ]+1,即next[ j ] = next[ x ]+1,否则
x= next [ next[ next[ j - 1 ] ] ],重复上面过程。
void getNext(string& T, vector<int>& next){
int j = 1;
int m = T.size();
next.resize(m, 0);
next[0] = -1;
for (;j < m;++j)
{
int val = 0;
if (next[j - 1]>=0&&T[j - 1] == T[next[j - 1]]) {
val = next[j - 1] + 1;
}
else {
int x = next[j - 1];
while (x>=0&&T[x] != T[j - 1]) {
x = next[x];
}
val = x + 1;
}
next[j] = val;
}
}
int KMP(string& S, string& T, int pos = 0) {
vector<int> next;
getNext(T, next);
int i = pos;
int j = 0;
int m = S.size();
int n = T.size();
while (i < m && j < n) {
if (S[i] == T[j]) {
++i;
++j;
}
else {
j = next[j];
if (j < 0) {
++i;
j = 0;
}
}
}
return j == n ? i - n : -1;
}
计算next值需要的时间复杂度为O(n),空间复杂度为O(n),因此KMP算法整体的时间复杂度为O(m+n),空间复杂度为O(n),其中m 为主串 S 的长度,n 为子串 T 的长度。
在大部分情况下,BF算法与KMP算法的时间复杂度相差不大,只有当主串与子串存在大量部分匹配时KMP算法的时间优势才会显现出来。