①KMP算法到底是用来干什么的?
KMP算法所做的事情,就是在字符串中寻找子串。比如ilovecode这个字符串中,我们可以搜寻到love这个子串。但如果用回溯的暴力方法寻找子串(即两个for循环),虽然思路简单,但是时间复杂度为O(M*N)。借助KMP算法,可将复杂度降为一个循环,增进了效率。
②学习KMP算法之前必须知道的东西
(1)前缀和后缀的概念:
*前缀:指的是从首字符开始往后的子字符串(不包括尾字符)。比如lwcsb的前缀就是l、lw、lwc和lwcs,lwcsb不是前缀。
*后缀:与前缀相同,类推。lwcsb的所有后缀有 b , sb , csb , wcsb 。
(2)最长相等前后缀的概念:
比如aaba,前缀有a , aa , aab;后缀有a , ba , aba。
上下(前后)匹配的最长长度1。
(3)前缀表:
就是一个字符串从前到后所有前缀字符串的最长相等前后缀数字所组成的表。
③前缀表(next数组)的实现:
实现KMP的高效算法,我们必须借助前缀表,也就是所谓的“next数组”。
用两个遍历头 i , j 指示前后缀的末位。注意,i既表示前缀的末位,也表示前缀的长度,利用这一点,我们可以对应填入next数组的值。在填充过程中,我们需要讨论两种情况:
<1>i,j所指示的字符不相等:i需要退位,退到相应位置之后再进行赋值。
<2>i,j所指示的字符相等:i进位,用i赋值next[j]。
【上述两点总结起来就是:(i退位 -> 赋值 -> j自增)和(i自增 -> 赋值 -> j自增)】
*光这样说说肯定不知所云一头雾水。让我来将这个过程实例化:
*****可以看出,整个这一个创建next数组(前缀表)的过程,其实有四个关键:
<1>初始化:i,j和next[0]=0。
<2>判断讨论。
<3>j循环后移,i持续退位。
<4>赋值:next[j]=i。(原理是:i本身的值就表示了最长公共子串的长度!)
理解了这些内容,代码顺理成章:
void Get_Nextarray(char str[],int len,int next[]) //获取字符串str[]的前缀表next[]
{
int i,j;
i=0; j=1; //初始化
next[0]=0;
for(j=1; j<=len-1; j++) //for循环,控制j向后
{
if (str[i]==str[j]) //相同:i后移
{
i++;
}
else if (str[i]!=str[j]) //不相同
{
while(str[i]!=str[j] && i>=1) //i持续退位(注意i>=1的限制)
{
i=next[i-1]; //看i所指元素的前一个元素的next值
}
}
else continue;
next[j]=i; //赋值
}
}
④KMP算法的真正实现:
我们要使用之前建立的“前缀表”。如何使用呢?
长串与子串一同遍历,直到遍历到出现不同元素为止。此时子串调用next数组,子串遍历开始的下标变为next[j-1](即当前元素的前一个元素next数组元素的数字,这个数字表示接下来开始遍历的子串的起始位置的下标)
——让ch[m]子串去匹配str[n]长串。
(1)定义两个遍历头i和j,分别遍历str和ch。
(2)全过程分为两个过程:同步遍历和ch子串的跳转(使用next)
(3)while循环控制str和ch的同步遍历,一旦出现不相等的情况,ch子串就要跳转(看前一个元素的next),i与j重新对其,继续执行while的循环。
(4)结束标志:如果i控制的子串能够遍历到串尾,那么说明str中找到了对应的子串,那么搜寻成功,跳出循环,结束程序;如果j控制的长串遍历结束,i控制的子串也没能遍历到末尾,那么搜寻失败,结束程序。
请看代码:
#include<stdio.h>
#include<string.h>
void Get_Nextarray(char str[],int len,int next[]) //获取字符串str[]的前缀表next[]
{
int i,j;
i=0; j=1; //初始化
next[0]=0;
for(j=1; j<=len-1; j++) //for循环,控制j向后
{
if (str[i]==str[j]) //相同:i后移
{
i++;
}
else if (str[i]!=str[j]) //不相同
{
while(str[i]!=str[j] && i>=1) //i持续退位(注意i>=1的限制)
{
i=next[i-1]; //看i所指元素的前一个元素的next值
}
}
else continue;
next[j]=i; //赋值
}
}
int main(void)
{
/*子串前缀表测试程序
int n;
scanf("%d",&n);
getchar();
char str[n],next[n];
gets(str);
Get_Nextarray(str,n,next);
int i;
for (i=0; i<=n-1; i++)
{
printf("%d ",next[i]);
}
*/
char str[1000]; //长串
char ch[1000]; //子串
printf("Please input Long String:"); gets(str);
printf("Please input Substring:"); gets(ch);
printf("\n-------------------------------------\n");
int LEN=strlen(str);
int len=strlen(ch);
int next[len]; //子串前缀表
Get_Nextarray(ch,len,next);
int i,j; //i遍历长串str,j遍历子串ch
i=j=0;
int flag=0; //标志变量
while(j<=len-1)
{
if (j==len-1 && str[i]==ch[j]) //一次匹配完,输出位置,继续往后匹配
{
printf("Found the pattern at : %d -> %d \n",i-j+1,i+1); //输出位置(+1是为了看得舒服,按数组来说位置是i-j)
j=next[j-1];
flag++;
}
if (str[i]==ch[j]) //相同则往后继续遍历
{
i++;
j++;
}
else //不相同,则j控制的子串需要跳转
{
if (j==0) //特殊情况是j回到了头也无法匹配,这个时候只要i移动了
{
j=0;
i++;
}
else
{
j=next[j-1];
}
}
if (i==LEN-1) break;
}
//出循环后,如果标志变量为0,说明一次也没有匹配到。反之已经匹配到。
if (flag==0)
{
printf("-------------------------------------\n"); //形式主义
printf("Not Found!\n\n");
}
else
{
printf("-------------------------------------\n");
printf("Found the pattern %d time(s) in the string!\n\n",flag);
}
return 0;
}
测试样例输出: