字符串匹配和 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;
}