问题描述
给定一个模式串 t = t 0 t 1 ⋯ t n t=t_0t_1 \cdots t_n t=t0t1⋯tn,目标串 s = s 0 s 1 ⋯ s m s=s_0s_1 \cdots s_m s=s0s1⋯sm,要求找出模式串在目标串的出现位置(字符串匹配)。
解决方案
暴力美学
for(int i = 0; i < m - n; i++){
bool flag = true;
for(int j = 0; j < n; j++){
if(s[i+j] != t[j]) flag = false;
}
if(flag) break;
}
KMP
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth
,J.H.Morris
和 V.R.Pratt
提出的。
下图展示了字符串匹配的过程:
按照暴力的算法,当模式串和目标串发生失配时,模式串会从头开始进行匹配,事实上,这样可能存在冗余的步骤。假设模式串满足
t
0
t
1
⋯
t
k
−
1
=
t
j
−
k
t
j
−
k
+
1
⋯
t
j
−
1
t_0t_1\cdots t_{k-1}=t_{j-k}t_{j-k+1}\cdots t_{j-1}
t0t1⋯tk−1=tj−ktj−k+1⋯tj−1,根据
t
0
t
1
⋯
t
j
−
1
=
s
i
−
j
s
i
−
j
+
1
⋯
s
i
−
1
t_0t_1\cdots t_{j-1}=s_{i-j}s_{i-j+1}\cdots s_{i-1}
t0t1⋯tj−1=si−jsi−j+1⋯si−1,因此有以下关系
t
0
t
1
⋯
t
k
−
1
=
s
i
−
k
s
i
−
k
+
1
⋯
s
i
−
1
t_0t_1\cdots t_{k-1}=s_{i-k}s_{i-k+1}\cdots s_{i-1}
t0t1⋯tk−1=si−ksi−k+1⋯si−1,那么此时模式串就没有必要从头开始,只需回退到
k
k
k的位置即可。
通过观察发现回退的位置只和模式串相关,与目标串无关。因此求得模式串的每一个元素的最终回退位置,将会加快求解过程。
next数组求解
n
e
x
t
next
next数组记录的应当是模式串中每个元素发生失配时最终回退位置,同时
n
e
x
t
[
j
]
next[j]
next[j]满足
t
0
t
1
⋯
t
k
−
1
=
t
j
−
k
t
j
−
k
+
1
⋯
t
j
−
1
t_0t_1\cdots t_{k-1}=t_{j-k}t_{j-k+1}\cdots t_{j-1}
t0t1⋯tk−1=tj−ktj−k+1⋯tj−1。
假设
n
e
x
t
[
j
]
=
k
next[j] = k
next[j]=k, 现在我们要求
n
e
x
t
[
j
+
1
]
next[j+1]
next[j+1]:
- 当
t
[
j
]
=
=
t
[
k
]
t[j] == t[k]
t[j]==t[k]时,由于存在
n
e
x
t
[
j
]
=
k
next[j] = k
next[j]=k即
t
0
t
1
⋯
t
k
−
1
=
t
j
−
k
t
j
−
k
+
1
⋯
t
j
−
1
t_0t_1\cdots t_{k-1}=t_{j-k}t_{j-k+1}\cdots t_{j-1}
t0t1⋯tk−1=tj−ktj−k+1⋯tj−1,可以得到
t
0
t
1
⋯
t
k
=
t
j
−
k
t
j
−
k
+
1
⋯
t
j
t_0t_1\cdots t_{k}=t_{j-k}t_{j-k+1}\cdots t_{j}
t0t1⋯tk=tj−ktj−k+1⋯tj即
n
e
x
t
[
j
+
1
]
=
k
+
1
next[j+1] = k+1
next[j+1]=k+1。
- 当
t
[
j
]
!
=
t
[
k
]
t[j] != t[k]
t[j]!=t[k]时,求解
n
e
x
t
[
j
+
1
]
next[j+1]
next[j+1],需要找满足
t
0
t
1
⋯
t
k
′
−
1
=
t
j
−
k
′
+
1
t
j
−
k
′
+
2
⋯
t
j
t_0t_1\cdots t_{k'-1}=t_{j-k'+1}t_{j-k'+2}\cdots t_{j}
t0t1⋯tk′−1=tj−k′+1tj−k′+2⋯tj的
k
′
k'
k′。由于
t
0
t
1
⋯
t
k
−
1
=
t
j
−
k
t
j
−
k
+
1
⋯
t
j
−
1
t_0t_1\cdots t_{k-1}=t_{j-k}t_{j-k+1}\cdots t_{j-1}
t0t1⋯tk−1=tj−ktj−k+1⋯tj−1且
t
[
j
]
!
=
t
[
k
]
t[j] != t[k]
t[j]!=t[k],那么求解满足
t
0
t
1
⋯
t
k
′
−
1
=
t
j
−
k
′
+
1
t
j
−
k
′
+
2
⋯
t
j
t_0t_1\cdots t_{k'-1}=t_{j-k'+1}t_{j-k'+2}\cdots t_{j}
t0t1⋯tk′−1=tj−k′+1tj−k′+2⋯tj的
k
′
k'
k′就等于模式串
t
0
t
1
⋯
t
k
t_0t_1\cdots t_{k}
t0t1⋯tk和目标串
⋯
t
j
−
k
t
j
−
k
+
1
⋯
t
j
\cdots t_{j-k}t_{j-k+1}\cdots t_{j}
⋯tj−ktj−k+1⋯tj在
j
j
j这个位置发生失配,模式串需要回退的最终位置
k
′
′
k''
k′′加上1即
k
′
=
k
′
′
+
1
=
n
e
x
t
[
k
]
+
1
k'=k''+1=next[k]+1
k′=k′′+1=next[k]+1。(事实上,可能不会一次回退成功,需要不断比较
t
[
j
]
=
=
t
[
k
′
′
]
t[j] == t[k'']
t[j]==t[k′′], 如果存在满足等于,其实就回到前面第一点分析的内容;如果一直不等,就会到达边界。)
KMP 代码
求解 n e x t next next数组
void getNext(vector<int> &next, string t)
{
int j = 0,k = -1;
next[0] = -1;
while(j < t.length()-1)
{
if(k == -1 || t[j] == t[k])
{
j++; k++;
next[j] = k;
}
else k = next[k];
}
}
KMP
int KMP(string s, string t)
{
vector<int> next(t.length());
int i = 0, j = 0;
getNext(next, t);
while(i < s.length() && j < t.length())
{
if(j == -1 || s[i] == t[j])
{
i++;
j++;
}
else j = next[j];
}
if( j >= t.length())
return (i - t.length());
else
return (-1);
}
优化求解next数组
前面说过, n e x t next next数组存储的是模式串中每一个元素的最终回退位置。
void getNext(vector<int> &next, string t)
{
int j = 0,k = -1;
next[0] = -1;
while(j < t.length()-1)
{
if(k == -1 || t[j] == t[k])
{
j++; k++;
if(t[j] == t[k]) next[j] = next[k];
else next[j] = k;
}
else k = next[k];
}
}