KMP算法及优化

KMP算法及优化

今天看到同学在复习数据结构书上的KMP算法,忽然发觉自己又把KMP算法忘掉了,以前就已经忘过一次,看样子还是没有真正的掌握它,这回学聪明点,再次搞明白后记录下来。

一般字符串匹配过程

KMP算法是字符串匹配算法的一种改进版,一般的字符串匹配算法是:从主串(目标字符串)模式串(待匹配字符串)的第一个字符开始比较,如果相等则继续匹配下一个字符, 如果不相等则从主串的下一个字符开始匹配,直到模式串被匹配完,则匹配成功,或主串被匹配完且模式串未匹配完,则匹配失败。匹配过程入下图:
在这里插入图片描述

这种实现方式是最简单的, 但也是低效的,因为第三次匹配结束后的第四次和第五次是没有必要的。

分析

第三次匹配在j = 0(a)和i = 2(a)处开始,在j = 4( c )和i = 6(b)处失败,这意味着模式串和主串中:j = 0(a)和i = 2(a)、j = 1(b) 和 i = 3(b)、j = 2( c )和 i = 4( c )、j = 3(a) 和 i = 5( a )这四个字符相互匹配。

分析模式串的前3个字符:模式串的第一个字符j = 0是a,j = 1(b)、j = 2( c )这两个字符和j = 0 (a )不同,因此以这两个字符开头的匹配必定失败,在第三次匹配中,主串中i = 3(b)、i = 4( c )和模式串j = 1(b)、j = 2( c )相互匹配,因此匹配失败后,可以直接跳过主串中i = 3(b)、i = 4( c )这两个字符的匹配。

继续分析模式串的 j = 3( a ) 和 j = 4( c ) 这两个字符,如果模式串匹配到j = 4( c )这个字符才失败的话,因为j = 4©的前一个字符j = 3(a)和第一个字符 j = 0( a )是相同的,结合上一个分析得知:

1):下一次匹配中主串已经跳过了和j = 3(a)前两个相互匹配的字符i = 3( b )、i = 4( c ),将从i = 5(a)开始匹配。
2):j = 3(a)和i = 5(a)相互匹配。

因此下一次匹配认为j = 3(a)和i = 5(a)已经匹配过了,匹配从j = 4(b)和i = 6(b)开始,这样的话也跳过了j = 3(a)这个字符的匹配。

同理可得第二次匹配也是没必要的。

KMP算法

KMP算法匹配过程

利用KMP算法匹配的过程如下图:
在这里插入图片描述

KMP算法的改进之处在于:能够知道在匹配失败后,有多少字符是不需要进行匹配可以直接跳过的,匹配失败后,下一次匹配从什么地方开始能够有效的减少不必要的匹配过程。
在这里插入图片描述

在得到子串前缀和后缀的最长公共匹配字符数l后,以后在i = x,j = n处匹配失败时,可以直接从i = x,j = l处继续匹配(证明过程参考:严蔚敏的《数据结构》4.3章),这样问题就很明显了,我们要求出n和l对应的值,其中n是模式串字符数组的下标,l的有序集合通常称之为next数组,前面两个模式串的next数组和下标n的对应如下:
在这里插入图片描述

模式串2完整匹配过程

有了这个next数组,那么在匹配的过程中我们就能在j = n处匹配失败后,根据next[n]的值进行偏移,其中next[0]固定为-1,代表在当前i这个位置整个模式串和主串都无法匹配成功,要从下一个位置i = i + 1及j = 0处开始匹配,模式串2的匹配过程如下:
在这里插入图片描述

现在知道了next数组的作用,也知道在有next数组时的匹配过程,那么剩下的问题就是如何通过代码求出next数组及匹配过程了。

next数组的过程可以认为是将模式串拆分成n个子串,分别对每个子串求前缀和后缀的最长公共匹配字符数l,这一点可以通过上图(最长公共匹配字符数)看出来(没有画出l=0时的图解)看出来。

代码如下

next数组的代码如下:

//改进前
void get_next(String T , int *next )
{
    int i = 1;
    int j = 0 ;
    next[1] = 0 ;
    while( i < T[0])
    {
        if( j == 0 || T[i] == T[j])
        {
            i++;
            j++;
            next[i] = j ;
        }
        else
        {
            j = next[j];
        }
    }

}

根据next数组求模式串在主串中的位置代码如下:


//返回字串T在主串s第pos个位置
int Index_KMP (String S , String T , int  pos)
{
    int i = pos ;
    int j = 1;
    int next[225];
    get_next2(T , next);
    while( i <= S[0] && j <= T[0]){
        if(j == 0 ||  S[i] == T[j])
        {
            i++;
            j++;
        }
        else
        {
            j = next[j];
        }
    }
    if( j > T[0])
        {
            return i - T[0];
        }
        else
        {
            return 0;
        }


}

测试代码如下:

int main()
{
    char str[255] = " ababaaaba";
    char str1[100] = " aab";
    int next[255];
    int i = 1 ;

    //字串和母串[0] 是用来保存字符串的长度方便使用 ;
    str[0] = 10;
    next[0] = 0;
    str1[0] = 3;
    get_next( str1 , next) ;
    printf("next 的值:\n");
    for(i = 1 ; i < 4 ; i++)
    {
        printf("%d  ", next[i]);
    }
    printf("\n-------------\n");
    printf("子串在母串第一次出现的位置为\n%d\n",Index_KMP ( str , str1, 0));


}

在这里插入图片描述

KMP算法优化

再回过头去看模式串2的next数组的图:
在这里插入图片描述

如果模式串和主串的匹配在j = 6(b) 处失败的话,根据j = next[6] = 1得知下一次匹配从 j = 1 处开始,j = 1处的字符和j = 6处的字符同为c,因此这次匹配必定会失败。
同样的,模式串和主串的匹配在j = 7©处或在j = 9(b)处失败的话,根据next数组偏移后下一次匹配也必定会失败。

考虑如果模式串是: aaaac,根据一般的KMP算法求出的next数组及匹配过程如下:
在这里插入图片描述

显而易见,在第二次匹配失败后,第三、四、五次匹配都是没有意义的,j = next[3]、j = next[2]、j = next[1]、j = next[0]这四处的字符都是a,在j = 3(a)处匹配失败时,根据模式串本身就应该可以得出结论:可以跳过j = 2(a)、j = 1(a)、j = 0(a)的匹配,直接从i = i + 1 、j = 0处开始匹配.

所以优化过后的next数组应该是:

在这里插入图片描述

//改进后
void get_next2(String T , int *next )
{
    int i = 1;
    int j = 0 ;
    next[1] = 0 ;
    while( i < T[0])
    {
        if( j == 0 || T[i] == T[j])
        {
            i++;
            j++;
            if(T[i] != T[j] )
            {
               next[i] = j ;
            }
            else
            {  // aaaaaaabb  如果最后一个a不匹配,前面也不匹配
                next[i] = next [j] ;
            }
        }
        else
        {
            j = next[j];
        }
    }

}

完整代码

#include <stdio.h>
typedef char *String;
//改进前
void get_next(String T , int *next )
{
    int i = 1;
    int j = 0 ;
    next[1] = 0 ;
    while( i < T[0])
    {
        if( j == 0 || T[i] == T[j])
        {
            i++;
            j++;
            next[i] = j ;
        }
        else
        {
            j = next[j];
        }
    }
}

//改进后
void get_next2(String T , int *next )
{
    int i = 1;
    int j = 0 ;
    next[1] = 0 ;
    while( i < T[0])
    {
        if( j == 0 || T[i] == T[j])
        {
            i++;
            j++;
            if(T[i] != T[j] )
            {
               next[i] = j ;
            }
            else
            {  // aaaaaaabb  如果最后一个a不匹配,前面也不匹配
                next[i] = next [j] ;

            }
        }
        else
        {
            j = next[j];
        }
    }

}

//返回字串T在主串s第pos个位置
int Index_KMP (String S , String T , int  pos)
{
    int i = pos ;
    int j = 1;
    int next[225];
    get_next2(T , next);
    while( i <= S[0] && j <= T[0]){

        if(j == 0 ||  S[i] == T[j])
        {
            i++;
            j++;
        }
        else
        {
            j = next[j];

        }

    }
    if( j > T[0])
        {
            return i - T[0];
        }
        else
        {
            return 0;
        }
}
int main()
{
    char str[255] = " ababaaaba";
    char str1[100] = " aab";
    int next[255];
    int i = 1 ;

    //字串和母串[0] 是用来保存字符串的长度方便使用 ;
    str[0] = 10;
    next[0] = 0;
    str1[0] = 3;
    get_next( str1 , next) ;
    printf("next 的值:\n");
    for(i = 1 ; i < 4 ; i++)
    {
        printf("%d  ", next[i]);
    }
    printf("\n-------------\n");
    printf("子串在母串第一次出现的位置为\n%d\n",Index_KMP ( str , str1, 0));
	getchar(); 

}

后记

个人理解 理解 如果还不清楚请参考动态规划之KMP字符匹配算法

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章