数据结构 - 模式串匹配 - KMP算法
数据结构 - 模式串匹配 - KMP算法
〇、关于
- KMP算法:由D.E.Knuth,V.R.Pratt,J.H.Morris同时发现。[1]
- 部分内容参考《数据结构考研复习指导》·王道论坛
- KMP算法是对双指针暴力搜索算法的改进
- 文中序号皆为索引值,从0开始(因此结论会有区别)
- 文中的KMP算法基于部分匹配值表(PM表)
一、前置
示例字串:
abcac
- 前缀
指除去最后一个字符,字符串的所有头部字串。示例字串的前缀有 - 后缀
指除去最后一个字符,字符串的所有尾部字串。示例字串的后缀有 - 部分匹配值
指字符串的前缀和后缀的最长一致长度。示例字串的部分匹配值为0。
在字符串匹配过程中,已匹配字符串(和模式字符串一致的部分)的部分匹配值表示其利用价值。 - 部分匹配值表
将字符串的所有头部字串求部分匹配值,得到部分匹配值表(Partial Match, PM)。示例字串的PM表为:
部分匹配表
id | 0 | 1 | 2 | 3 | 4
- | - | - | - | - | -
S | a | b | c | a | c
PM | 0 | 0 | 0 | 1 | 0
二、已匹配字符串的特征
示例目标字符串:
ababcabcacbab
示例模式字符串:abcac
- 在进行字符串模式匹配过程中,当出现不匹配时,向前移动目标字符串指针。对于已匹配的字符串(也即一致的字符串),向前移动的距离需要参考最大的一致前后缀长度,例如:
ababcabcacbab
abcac
在匹配至第三个字符c
时,出现不匹配。此时已经匹配的字符串为ab
,其最大的一致前后缀长度为0,表示在字符串长度内,不会出现任何匹配,则下一个要匹配的位置就需要跳过整个已匹配字符串。为
ababcabcacbab
abcac
此时在匹配到第五个字符c
时,出现不匹配。此时已经匹配的字符串为abca
,其最大一致前后缀长度为1,表示已匹配的四个字符中,最后一个字符需要重新比较,则下一个需要匹配的位置就是最后一个字符。为
ababcabcacbab
abcac
三、已知PM表的匹配算法
由上述分析可知,当匹配失败时,下一个要匹配的位置需要移动的距离和最后一个已匹配字符的PM值和已匹配的字符串长度有关。
匹配失败时,记PM表为\(PM[ ]\),需要往回移动的距离为\(Move\),模式字符串指针为\(j\)(从0开始),则有
\(Move=j-PM[j-1]\)
将\(PM\)表右移一位得到表\(next[]\),则为
\(Move=j-next[j]\)
新的指针位置为
\(j=j-Move=next[j]\)
也即匹配失败时,直接将模式字符串指针移动至 \(next[j]\) (目标字符串指针不需要移动),然后重新开始比较。
四、求模式字符串的next表
由上述推导可知,next表可以看作是PM表右移一位得到。因此先推导PM表。
假设 \(PM[j]=k\) ,模式字符串为 \(p_0p_1p_2...p_{n-1}\) ,则有\(p_0...p_{k-1}=p_{j-k+1}...p_{j}\),且 \(k\) 为满足该式的最大值。
现在需要求 \(PM[j+1]\) ,
若 \(p[k]=p[j+1]\) ,则有 \(p_0...p_k =p_{j-k+1}...p_{j+1}\) , 即 \(PM[j+1]=k+1\) (由 \(PM[j]=k\) 可以推导)。
若 \(p[k] \not= p[j+1]\) ,则有 \(p_0...p_k \not=p_{j-k+1}...p_{j+1}\) (且更长的串也不相等)。对于 \(p_{j-k+1}...p_{j+1}\),由于 \(PM[j]=k\) ,因此可以写作 \(p_0...p_{k-1}p_{j+1}\)。对 \(p_0...p_{k-1}p_k\) 和 \(p_0...p_{k-1}p_{j+1}\),则其最长一致前后缀串必然包含了 \(p_0...p_{k-1}\) 的最长前后缀一致串(见下例)。
例如:
k j
12312x...12312y...
其中,PM[j] = k = 5
求最长重合串,首先考虑前k个字符的前后缀一致段,对于12312而言,就是PM[4]的值,
为2,即有两个字符是已知一致的,如下:
12312y
12312x
此时需要判断后一个字符是否相同。
如果不相同,则先考虑前2个字符,对于12,即PM[1],值为0,表示前2个字符无
前后缀一致段,则直接比较极限情况,即y和p[0]是否相同。
即获取 \(PM[k-1]\) 的值,若为0,则直接比较 \(p_{k+1}\) 和 \(p_0\),若不为0,则比较 \(p_{k+1}\) 和 \(p_{PM[k-1]}\) 。如果不相同,且 \(PM[k-1]>0\) ,则获取 \(PM[PM[k-1]-1]\) ,循环查找。
最后将 \(PM\) 表转换为 \(next\) 表即可。
五、基于PM表的一个实现
/**
* KMP算法
* D.E.Knuth, V.R.Pratt, J.H.Morris
*
* 有修改
*
求PM表:
i 0 1 2 3 4 5 6 7 8
对于模式串 1 2 1 1 2 3 1 2 1
PM 0 0 1 1 2 0 1 2 3
next -1 0 0 1 1 2 0 1 2
流程:
PM[0] = 0
i = 1, i表示下一个处理的子串
j = 0, j表示上一个处理的子串的PM值
p[j]!=p[i],而j==0,取pm[i]=j,即pm[1]=0
i = i+1 = 2
j = 0
p[j]==p[i],取pm[i]=j+1,即pm[2]=1
i = i+1 = 3
j = j+1 = 1
p[j]!=p[i],取j=pm[j-1],即j=pm[0]=0,
p[j]==p[i],取pm[i]=j+1,即pm[3]=1
i = i+1 = 4
j = 1
p[j]==p[i],取pm[i]=j+1,即pm[4]=2
i = i+1 = 5
j = 2
p[j]!=p[i],取j=pm[j-1],即j=pm[1]=0
p[j]!=p[i],而j==0,取pm[i]=j,即pm[5]=0
...
利用next表进行模式匹配
1 2 1 3 ... 目标字符串
1 2 1 1 2 3 ... 模式字符串
i 为目标字符串指针
j 为模式字符串指针
p[3]匹配失败,查next[3]=1,即
取j=1,继续进行匹配。
如果next为-1,则整体向前一步
(-1代表第一个字符就匹配失败)
*/
const char *strstr_kmp(const char *szSrc, const char *szToken) {
int nLenToken = strlen (szToken);
std::vector<int> pm(nLenToken); /* pm表 */
/* 求模式串的部分匹配(PM)表 */
pm[0] = 0;
int i = 1; /* [0,i]表示当前计算pm值的头部子串 */
int j = 0; /* j为上一个已处理字符串的PM值 */
while (i < nLenToken) {
if (szToken[j] != szToken[i]) {
if (j == 0) {
pm[i]=j;
++i;
}
else {
j = pm[j-1];
}
}
else {
pm[i] = j+1;
++i;
++j; /* j=pm[i] */
}
}
/* PM表转next表 */
std::vector<int> next(nLenToken); /* next表 */
next[0] = -1;
for (int k = 1; k < next.size(); ++k) {
next[k] = pm[k-1];
}
/* 利用next表进行模式匹配 */
i = 0; /* i表示szSrc指针 */
j = 0; /* j表示szToken指针 */
while (szSrc[i] != '\0' && szToken[j] != '\0') {
if (szSrc[i] == szToken[j]) {
++i;
++j;
}
else {
j = next[j];
if (j < 0) {
++i;
j = 0; /* ++j */
}
}
}
if (j == nLenToken) {
return szSrc + i - j;
}
else {
return nullptr;
}
}
六、KMP算法的优化
目标字符串:a a a b a a a a b
模式字符串:a a a a b
PM 0 1 2 3 0
next -1 0 1 2 3
匹配失败时,模式字符串向右滑动一位,由于前四个字符是相等的,因此重复了。
当模式字符串中出现了 \(p_j=p_{next[j]}\) 的情况时,对于PM表,也即如果出现 \(p_{j}=p_{pm[j-1]}\) 时,会引起重复比较。此时,取上一个PM值即可(算法中是递进式,因此上一个PM值以保证没有这种问题)。
/* 求模式串的部分匹配(PM)表 */
pm[0] = 0;
int i = 1; /* [0,i]表示当前计算pm值的头部子串 */
int j = 0; /* j为上一个已处理字符串的PM值 */
while (i < nLenToken) {
if (szToken[j] != szToken[i]) {
if (j == 0) {
pm[i]=j;
++i;
}
else {
j = pm[j-1];
}
}
else {
if (szToken[i+1] == szToken[j+1]) {
if (j == 0) {
pm[i] = 0;
}
else {
pm[i] = pm[j-1];
}
}
else {
pm[i] = j+1;
}
++i;
++j; /* j=pm[i] */
}
}
末、引用
摘自《数据结构》·严蔚敏,4.3.2 - 模式匹配的一种改进算法 ↩︎
posted on 2020-10-31 17:36 amazzzzzing 阅读(447) 评论(0) 收藏 举报