最近翻了翻严蔚敏老师的《数据结构(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
未找到匹配子串
请输入模式串: