普通的字符串匹配方法(串的模式匹配方法)
对于普通的串匹配方法,通过简单的例子进行解析
T: a b a c a a b a c a b a c
W: a b a c a b
会从T[0]跟W[0]进行匹配,如果相等则匹配W的下一个字符,直到出现不相等的情况。简单丢弃W[0]开始的匹配信息,然后从T[1]开始继续同W进行匹配,直到串结束或者满足W长度和T长度差值的循环,如果T中不够n个W自然可以结束。
具体实例如下所示,返回第一个匹配串及其后边的字符,时间复杂度O((m-n)*n):
const char* strFind(const char* string1, const char* substring)
{
assert(string1 != NULL && substring != NULL);
int m = strlen(string1);
int n = strlen(substring);
if (m < n)
return NULL;
for (int i = 0; i <= m - n; ++i)
{
int j = 0;
for (; j < n; ++j)//对substring循环,也就是上文中的W
{
if (string1[i + j] != substring[j])
break;
}
if (j == n)
return (string1 + i);
}
return NULL;
}
这种简单的丢弃前面的信息造成了极大的浪费和低下的匹配效率。
KMP算法是由D.E和V.R同时发现,当前算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体就是实现next()函数,函数本身包含了模式串的局部匹配信息。
针对第一种基本匹配方法,KMP方法对于每一个模式串会事先计算出模式串内部匹配信息,在匹配失败时最大移动模式串,以减少匹配次数。
如何计算右移距离成了算法的核心,右移距离的计算也就是next的计算,也就是输入串前缀后缀对应的匹配值的计算。
什么是前后缀?
“前缀”指除了最后一个字符以外,一个字符串的全部头部组合,“后缀”除了第一个字符以外,一个字符串的全部尾部组合。实例如下:
字符串:bread
前缀:b, br, bre, brea
后缀:read, ead, ad, d
部分匹配值的计算:
搜索词:A B C D A B D
部分匹配值就是“前缀”和“后缀”的最长共有元素的长度。根据以上搜索词为例:
A的前缀和后缀都为空集,共有元素的长度为零。
AB的前缀为A,后缀为B,共有元素长度为0。
ABC前缀为A, AB,后缀为BC,C共有元素长度为0.
ABCD前缀为A,AB,ABC,后缀为BCD,CD,D,共有元素长度为0。
ABCDA前缀为A,AB,ABC,ABCD,后缀为BCDA,CDA,DA,A共有元素为A,共有元素长度为1。
ABCDAB前缀A,AB,ABC,ABCD,ABCDA,后缀为BCDAB,CDAB,DAB,AB,B。共有元素为AB,长度为2.
部分匹配的实质是:有时候字符串头部和尾部会有重复,例如“ABCDAB”之中有两个“AB”,那么他的部分匹配值就是2(“AB”的长度)。搜索移动的时候第一个AB向后移动思维(字符串长度减去部分匹配值),第一个AB向后移动四位就可以到达尾部AB的位置。
得到:移动位数 = 字符串长度 – 部分匹配值。字符串长度和部分匹配值的求解分别都是针对已经匹配的模式串子串进行的求解,实例如下:
T: a b a c a a b a c a b a c
W: a b a c a b
T[5]与W[5]出现不匹配,T[0]—T[4]是匹配的。字符串长度为5,
前缀:a,a b,a b a,a b a c; 后缀:b a c a,a c a,c a,a; 共有元素为a,长度为1。
因此可求的移动位数为4。
Next求解代码:
void get_nextval(char const* ptrn, int plen, int* nextval)
{
int i = 0;
nextval[i] = -1;
int j = -1;
while(i < plen - 1)
{
if (j == -1 || ptrn[i]==ptrn[j])
{
++i;
++j;
nextval[i] = j;
}
else
{
j = nextval[j];
}
}
}
字符串查找实例如下所示:
T: a b a c a a b a c a b a c a b a a b b
W: a b a c a b
可求得W的next表为:
W | a | b | a | c | a | b |
| 0 | 0 | 1 | 0 | 1 | 2 |
关于next求解,加入第二项的a,其实就是求的aba的前缀后缀公共项的最大值。
a b a c a a b a c a b a c a b a a b b
a b a c a b
从第0项开始对比,直到找到不相等的项:发现T[5]和W[5]不相等,也就是说T[0]-T[4]相等,获取T[0]-T[4]的前缀后缀相同项,最大长度,查表可得为1.
移动位数 = 5 – 1 = 4;
a b a c a a b a c a b a c a b a a b b
a b a c a b
按照此规律反复执行。时间复杂度O(m+n)
代码如下:
int kmp_search(const char*src,int slen,const char* patn, int plen,const int* nextval, int pos)
{
int i = pos, j = 0;
while(i<slen && j<plen)
{
if (j == -1 || src[i] == patn[j])
{
++i;
++j;
}else
{
j = nextval[j];
}
}
if (j >= plen)
{
return i-plen;
}
else
{
return -1;
}
}