字符串匹配和 KMP 算法
字符串匹配和 KMP 算法
基本匹配方法
基本的字符串匹配,可通过简单的方式解决:
int find(char *s, char *p, int pos)
{
int i = pos; // 待搜索字符串下标
int j = 0; // 模式当前下标
int slen = strlen(s); // 待搜索字符串长度
int plen = strlen(p); // 模式长度
while (i < slen)
{
if (s[i] != p[j])
{
i = i - j + 1; // 不匹配,回溯
j = 0;
}
else
{
i++; // 匹配,下一个位置
j++;
if (j == plen)
{
return i - plen;
}
}
}
return -1;
}
KMP
基本匹配方法每次不匹配时,都需要从字符串下个位置开始。如果模式中有部分字符串相同,如: abcab ,那么 ab 这个可以不比较,接着从模式的 c 开始,从而减少时间复杂度。
KMP 算法对模式字符串共同前后缀进行预处理,先确定模式字符串某个字符串出现不匹配时,应该如何移动模式的下标 j。前后缀是指模式字符串的前后若干个字母,当前后缀相同时,那么只需要简单将指向前缀后一个字母进行比较即可。
当 si == pj
时,只需要简单地 i++, j++
进行下一次比较,问题在于两者不同时。如下,
s0 s1 .....s(i-1-k).....s(i-1) si.................. sn
p0...p(j-1-k).....p(j-1) pj.....pm
p0..........p(k-1) pk..pj...pm
当 si != pj
,需要确定一个 k,使得有 p0...p(k-1) = s(i-1-k)...s(i-1)
,这样,就只需要 si 与 pk 进行比较,使得 i 不需要回溯,减少时间复杂度。而确定这个 k 的方法,在 KMP 中称为 next 函数或跳转表,即 k = next[j]。
当 pj 之前的字符串不为空,那么还可以得到 s(i-1-k)...s(i-1) = p(j-1-k)...p(j-1)
,因此有两个模式子字符串相等:
p0...p(k-1) = p(j-1-k)...p(j-1)
前缀 后缀
因此可见,当 k 取得最大时,得到的加速度最大,而这个 k 与 s 无关,只与模式字符串 p 有关。
当 pj 之前的字符串为空,即 j=0 时,这时相当于 si 和 p0 比较,那么此时的 k 就不能直接取 0 了,会导致 si 再次与 p0 比较而出现死循环,需要进行 i++ ,为了使得行为与匹配时类似,使此时 k = -1 ,那么可进行 i++, j++
,与匹配时处理行为相同。
现在,总结一下 kmp 的基本流程:
i = 0, j = 0;
while i < len(s):
if j = -1 or s[i] == p[j] then
i++,j++
if j == len(p):
return i - j // 找到了
else
j = next[j]
return -1
以及对于 next :
if j = 0 then
next[j] = -1
else
next[j] = MAX({k|k 满足 p0...p(k-1) = p(j-1-k)...p(j-1),1<=k<j)},集合不空)
or
next[j] = 0 其它情况,即从 p0 开始比较
next 跳转表
next 跳转表的理解要比对 kmp 的理解要麻烦。
先按照前面的讨论,
next[0] = -1
从模式匹配的角度来看现在的情况,p 同时成为主串和模式串,对于 next[i],有 p(i-1) == p(next[i-1]),如下
p0..................p(i-1) pi................pn
p0..........p(next[i-1]) p(next[i-1]+1)...........pn
p0.......p(j-1) p(j).........................pn
若 pi == p[ next[i-1] + 1]
, :
next[i] = next[i-1] + 1
而不等时,则要找到 k 使得:
p0...p(k-1) = p(i-1-k)...p(i-1)
然后比较 pi 与 pk。看上面 KMP 对于找 k 的过程,这个过程与上面是相同的。
总结上述过程:
next[0] = -1;
i = 1, k = 0
while i < len(p):
if k == -1 || p[i] == p[k], then
i++, k++
next[i] = k
else
k = next[k]
KMP 总结
汇总以上过程,写成函数为:
int* getNext(char *p)
{
int i, k;
int plen = strlen(p);
int *next = calloc(plen, sizeof(int));
next[0] = -1;
i = 1, k = 0;
while (i < plen)
{
if (k==-1 || p[i] == p[k])
{
i++, k++;
next[i] = k;
}
else
{
k = next[k];
}
}
return next;
}
int kmp(char *s, char *p, int pos)
{
int i = pos;
int j = 0;
int slen = strlen(s);
int plen = strlen(p);
int *next = getNext(p);
while (i < slen)
{
if (j == -1 || s[i] == p[j])
{
i++, j++;
if (j==plen)
{
free(next);
return i-plen;
}
}
else
{
j = next[j];
printf("%s\n", s);
printf("%s\n", p);
printf("%d %d\n", i, j);
printf("\n");
}
}
free(next);
next = NULL;
return -1;
}