字符串匹配的改进算法——KMP算法(C/C++)

字符串匹配

关于什么是KMP操作,详情见

点我快速了解什么是KMP算法


KMP的基本思想

一般方法的弊端:

下面可以不看~~感兴趣可以看看~~,总之就是说:一般方法在面对重复率极高的串时匹配效率极低~

首先,当主串为 00000000000000000000000000000000000000000000000000001 ,而模式串为 00000001 时,由于主串中前七个字符为 0 ,模式串中前52个字符为 0 ,每一轮比较都在模式串的最后一个字符处失配,此时需要将指针(游标) i (指示主串),回溯到 i-6 的位置上,并从模式串的第一个字符开始重新比较。整个匹配过程 指针(游标) i 需要回溯45次, while 循环需要执行 46 * 8 * (Index * m)。在 worst situation下 时间复杂度为O(m*n)

问题在哪?!

每次失配之后,模式串偏移量太小,导致重复匹配。如果你看了上面那段废话,就可以理解为,指针每次失配之后都回溯至起始位置,导致效率低下!

解决办法:指针不再回溯,一次遍历主串与模式串匹配即可完成!!

数学分析

来一段数学分析,不感兴趣的同学下滑再下滑就行~~

正文开始:

指针如何不回溯?只需要将模式串失配后,沿着主串方向滑动一定的长度,使主串部分可以与模式串匹配,不需要回溯指针。

  假设主串为S[i],模式串为P[j],其中i指示主串的位置即为主串指针(浮标),j指示模式串的位置即为模式串指针(浮标)。

  在一次匹配之后,设第j个字符匹配失败,假设此时主串在指针(游标)i在不回溯的前提下,应与模式串中的第 k (k<j)个字符继续匹配;则一定会有如下关系式:

  P[1]P[2]...P[k-1] = S[i-k+1]S[i-k+2]...S[i-1]      ----(1)

  这次匹配的前一次的匹配结果:
  
  P[j-K+1]P[j-k+2]...P[j-1] = S[i-k+1]S[i-k+2]...S[i-1]        ----(2)

  联立式(1)和(2),消去主串可以得到:

  P[1]P[2]...P[k-1] = P[j-K+1]P[j-K+2]...P[j-1]

  根据此式子可以得出如下结论:
  
  主串中第i位应与模式串中第 k(k<j) 位对齐,且 k 仅与模式串自身有关,因此,匹配仅需从模式串中第 k 个字符和主串中第 i 个字符比较起继续进行。

如上述所得:此时应求模式串的k值!为了确定k,需要一个next(){next[j]=k}来存储模式串的每一个字符对应的k值~

define next():
    令next[j]=k,则next[j]表明当前模式中第j个字符与主串中相应字符“失配”时在模式中需重新和主串中该字符进行比较的字符的位置。
next[j] = 
		if(j==1)    0;
		// Max集合非空
		else if(Max{K|1<k<j && p[1]...p[k-1]=p[j-k+1]...p[j-1]}) 
				Max{K|1<k<j && p[1]...p[k-1]=p[j-k+1]...p[j-1]  
		else    1;  //其他情况

next数组详解

next的文字描述

废话又来了~不过可以看看~

首先,我们简要分析一下 next()的功能。
假设 i 指示主串,j 指示模式串, k 表示应滑动到的位置。
由我们上面得到的数学公式的结论:k 只与模式串自身有关。

	P[1]P[2]...P[k-1] ”=“P[j-K+1]P[j-K+2]...P[j-1] 

那么这个 k 具体又是什么东西呢?
简而言之,就是模式串在第 j 个位置(失配的位置)之前,有 k-1 个字符与模式串从首字符起 k-1 个字符相同,这也是上述数学公式的结论。
那么如何求得模式串的 k 呢?
注意:需要求解的是模式串的第 j-(k-1)j-1 的位置与 1k-1 的位置字符相同
如果使用两层循环求解效率过低,在此处可以使用递归的思想。

即要求 k 这个位置,在每次比较,即模式串与模式串自己匹配(模式串既作为主串,也作为模式串),而我们的next()本身指示的就是模式串应该滑到哪个位置。
所以,应该使用两个指针指示模式串,假设分别为 iji 依次指向模式串的每一个位置, j 指向模式串头地址并听从 next()本身滑动。根据上述定义,模式串的首字符的 next 值应为0i从第 next[1] 开始,jnext[0]开始。

i 指示到模式串中间某个位置时,j 也指示到了模式串中的某个位置(j<i),此时对于next()应有两个选择:一、当前位置下,next[i] == next[j],那么只需要将指针顺移,并且该位置(i指示的位置)的next值就为 j 的值(前面一共判断相同判断了j次)。二、当前位置下,next[i] != next[j],此时,根据next()的定义,j 应该回溯到 next[j] 的位置(这里使用了递归)。

现在从初始状态检查 next[ ]。已知next[1] = 0。初始 j = 0i = 1 ,但next[0]并不存储数据,但是又要为next[1]赋值,避免使用冗长的判断,赋值,则在判断 next[i] 是否等于 next[j] 时,当 j=0 ,直接递增 i,与 jnext[0] 并没有实际意义,next[1] 才有实际意义)。

(这里只是在求模式串的k值,不要与主串,模式串的ij搞混,虽然本质上没有区别)

next实现:

next()的C语言代码如下:

// 求模式串T的next函数值并存入数组next。
void get_next(char *T, int **next){
    int i=0,j=-1,T_len = strlen(T);
    (*next) = (int *)malloc(T_len*sizeof(int));
    (*next)[0] = -1;
    while(i<T_len-1){
        if(j==-1||T[i]==T[j]){
            i++;j++;
            (*next)[i] = j;
        }
        else
            j = (*next)[j];
    }
    //for(int i=0;i<strlen(T);++i)
    //    printf("%d ",(*next)[i]);
    //putchar('\n');
}

KMP算法C语言实现

代码如下:

// 从主串S的第pos个字符后开始匹配即数组的pos-1个位置
int Index_Kmp(char *S,char *T,int *next,int pos){
    int i=pos-1,j=0;
    int S_len = strlen(S),T_len = strlen(T);
    while(i<S_len && j<T_len){
        if(j==-1 || S[i]==T[j]){
            ++i;
            ++j;
        }
        else 
            j = next[j];
    }
    if(j>=T_len)
        return i-T_len;
    else 
        return -1;
}
posted @ 2022-03-25 17:21  ymj68520  阅读(165)  评论(0)    收藏  举报
分享到: