KMP字符串模式匹配算法解析

最近翻了翻严蔚敏老师的《数据结构(c语言版)》,发现在读KMP算法时理解起来不是特别直观,所以就从自己的理解整理了一下KMP算法,希望能帮到有疑惑的同学。

观察某一时刻的匹配情况:

1. 假设从待匹配字符串 str 的第 i 位、模式字符串 pattern 的第 0 位开始,两者顺序进行匹配;

2. 假设 str 和 pattern 的前  j -1 位都匹配成功,但是第 j 位匹配失败,此时 str 的 i 指针走到了 i + j 处;

3. 如果采用暴力匹配算法,那么算法此时让 str 的指针直接回退到第 i + 1 位,pattern 再从第 0 位开始,然后重复上述步骤直到匹配完 pattern (匹配成功)或扫描完 str (匹配失败);

4.  如果采用KMP匹配算法,那么算法此时不是让 str 的指针回退到第 i + 1 位,而是仍然停留在第 i + j  位(即匹配失败的那一位),然后令 j =  pos[ j ](这个pos[ ]其实和严蔚敏老师的 next[ ] 是类似的,但是 next[ ] 理解起来不是特别直观,所以我用了一个 pos[ ] 表示,后文会提到如何生成pos[ ]),继续从 pattern 的 第 j 位 和 str 的第 i + j 位开始匹配。pos[ ] 表的含义是当 pattern 的第 j 位匹配失败时,就从 pos[ j ] 中读出一个值,这个值代表下一次从 pattern 的哪个位置开始再和 str 的第 i + j 位进行顺序匹配。

解释:第 4 步的原理是这样的,我们举个具体的例子

假设 str = *****abcabd*****,假设第一个 a 是str 的第 i 位;

假设 pattern = abcabe,下表是 pattern 的 位置和字符的对应情况:

0

1

2

3

4

5

a

b

c

a

b

e

ps.此例中j = 5 , str 中第 i + 5 位的符号 d 即是和pattern 匹配失败的那一位;

因为 str 和 pattern 匹配到第 5 位时匹配失败,但是我们发现已经匹配成功的部分 [ abcab ] 的前两位和后两位是完全一样的,同时这也是我们能找到的首部和尾部最长的重叠部分(下图中下划线部分,在此例中这个长度是 2),所以我们就可以直接把 pattern 向右滑动到首尾重叠的部分的下一位再开始匹配,如下所示:

*****abcab|d*****

              ab|cabe

即把 pattern 向右滑动,使得 pattern 匹配成功的部分首尾重叠达到最长, str 仍然保持在第 i + 5 位(即匹配失败的那一位),然后从 pattern 的第 2 位开始继续匹配。

在我们的例子中,此时 pattern 的失败位是第 5 位,下一次从 pattern 的第 2 位开始匹配,所以 pos[5] 就等于2。

那么算法这部分的代码我们就可以写出来了,如下所以:

while(i < (int)strlen(str) && j < (int)strlen(pattern))

{
        if(str[ i ] == pattern[ j ]) {   i++;j++;//匹配成功时 i 和 j 都向后移动一位 }
        else  j = pos[ j ];//匹配失败时查找位置表,确定下一步从 pattern 的哪一位开始匹配
}

那么现在的新问题就是如何计算出 pos[ ] 这个位置表了。


基本前提:

1. 首先, pos[ ] 的长度就是 pattern 的长度,每一位的值代表当该位匹配失败时,下一步再从 pattern 的哪一位开始;

2. pos[ ] 表的计算其实就是在 pattern 中再做一次自身的模式匹配,即同样定义两个指针 i 和 j ,把第 0 到 第 i  位之间的串作为 str ,第 0 到 第 j  位之间的串作为子 pattern 进行匹配。查看 pattern[ i ] 是否等于 pattern [ j ],如果相等,那么就说明 pattern 的第 i  + 1位之前存在 j + 1 位的首尾重叠部分,那么当其他的 str 和 pattern 在 pattern 的第 i  + 1 位不匹配时,因为 pattern 的前 j + 1 位和 后 j + 1 位是重叠的,所以下一步就可以直接从 pattern 的第 j + 1 位开始匹配;如果不相等,那么再查看 pos[ j ] 以确定从 子pattern 的哪一位开始和 str(原 pattern) 进行匹配;


计算过程:

1. 当其他 str 和 pattern 的第 0 位匹配失败时,很明显 pattern 的下一步仍然应该从第 0 位开始,但是 i 也要向后滑动一位;

2. 当其他 str 和 pattern 的第 1 位匹配失败时,同样 pattern 的下一步是从第 0 位开始,但是 i 保持不动。因此,可令 pos[ 0 ] = -1 , pos[ 1 ] = 0 ,以区分这两种情况;

3. 当 pattern[ i ] == pattern[ j ] 时,说明 pattern 的 [ 0 , 1 , 2 ,… , j ]  位和 [ i – j , … , i – 2 , i – 1, i ] 是重叠的,那么当其他 str 和 pattern 在 pattern 的第 i  + 1 位发生不匹配时,pattern 就可以直接滑动到 pattern 的第 j + 1 位开始匹配,所以此时可令 pos[  i + 1 ] = j + 1,同时把 i 和 j 各向后移动一位,以备计算 pos[ i + 2];

4. 当 pattern[ i ]  != pattern[ j ] 时,那么就要在已匹配部分 pattern [ i – j ,…, i – 3  , i – 2 , i-1 ](或pattern [ 0 , 1 , 2 , … , j – 1] ) 中寻找最长的首尾重叠部分,然后令 j 等于重叠部分的下一位置继续进行 pattern[ i ] 和 pattern[ j ] 的匹配,其实质就是查找 pos[ j ],然后令 j =  pos[ j ];

5. 当 j == -1  时,说明下一步只能从 pattern 的第 0 位开始匹配,所以此时令 pos[ i ] = 0,同时将 i 向后滑动一位,并令 j = 0 , 准备开始 pos[ i + 1 ] 的计算;

计算 pos[ ] 表算法如下:

void getPos(char pattern[ ], int pos[ ])
{
    pos[ 0 ] = -1;  pos[ 1 ] = 0;

    int i = 1;   int j = 0;

    while(i < strlen(pattern))
    {
        if( j ==  -1) { pos[ i+1 ] = 0;  i++;  j = 0;  }
        if(pattern[ i ] == pattern[ j ]) {  pos[ i+1 ] = j + 1;   i++;   j++;  }
        else    j = pos[ j ];
    }
}

考虑到 pos[ 0 ] =  -1 ,当 str 和 pattern 进行匹配的时候,如果遇到 pos[ 0 ],要把 i 指针向后滑动一位,并令 j = 0 ,所以匹配算法修改如下:


 while(i < (int)strlen(str) && j < (int)strlen(pattern))

{
        if(j == -1)  {  j = 0;   i++;   }

        if(str[i] == pattern[j])  {  i++;  j++; }
        else  j = pos[j];

完整代码如下:

#include <stdio.h>

#include <string.h>

int index(char str[], char pattern[], int pos[])

{
    int i = 0 ;  int j = 0 ;
    
    while(i < (int)strlen(str) && j < (int)strlen(pattern))
    {
        if(j == -1) {    j = 0;   i++;   }

        if(str[i] == pattern[j]) {  i++;  j++; }
        else   j = pos[j];
    }
    if(j == strlen(pattern))   return i – j;
    else   return -1;
}

void getPos(char pattern[], int pos[])
{
    pos[0] = -1;  pos[1] = 0;

    int i = 1;  int j = 0;

    while(i < strlen(pattern))
    {
        if(j == -1)  {  pos[i+1] = 0;  i++;   j = 0; }
        if(pattern[i] == pattern[j]) {  pos[i+1] = j + 1;  i++;  j++;  }
        else   j = pos[j];
    }
}

int main()
{

    char *pattern = (char *)malloc(sizeof(char)*20);
    char *str = (char *)malloc(sizeof(char)*100);
    int *pos = (int *)malloc(sizeof(char)*20);

    printf(“请输入模式串:\n”);
    while(scanf(“%s”,pattern) != EOF)
    {
        printf(“请输入待匹配串:\n”);
        scanf(“%s”,str);
        getPos(pattern, pos);

        int j = 0 ;
        printf(”    pos[] = “);
        while(j < strlen(pattern)) printf(“%d “,pos[j++]);
        putchar(‘\n’);
     
        int i = index( str,  pattern,  pos);
        if(i == -1)   printf(”    未找到匹配子串\n”);
        else printf(”    首位置:%d\n”,i);
        
        j = 0 ;
        while(j < 20) { pattern[j] = ‘\0’; pos[j++] = 0 ;}
        j = 0 ;
        while(j < 100) str[j++] = ‘\0’;
        putchar(‘\n’);
        printf(“请输入模式串:\n”);
    }

    return 0;
}

输出如下:

请输入模式串:
bcdabe
请输入待匹配串:
grabcdababcdabe
       pos[] = -1 0 0 0 0 1 2
       首位置:9

请输入模式串:
aaaa
请输入待匹配串:
egergaaaaaaaaafeg
       pos[] = -1 0 1 2 3
       首位置:6

请输入模式串:
bcd
请输入待匹配串:
fgh
       pos[] = -1 0 0 0
       未找到匹配子串

请输入模式串:

    原文作者:KMP算法
    原文地址: https://blog.csdn.net/u010097581/article/details/71447151
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞