一、思路
首先回顾以下朴素的查找算法,就是模式串P与文本串S匹配,当P的第0位和S的第0位比,P的第1位和S的第1位比,当他们不同的时候,P的第0位从S的第1位开始比,就这样直到比到P的最后一位。
KMP算法的巧妙之处在于不需要回溯文本串。当P[i]与S[j]不匹配的话,那么P[0~(i-1)]和S[(j-i)~(j-1)]是匹配的。如果在P[0~(i-1)]内有相同的前缀和后缀,那么如果像朴素算法一样一位一位的移动,到最终发生匹配一定是从P的最长相同前缀后缀的最后一个字符P[k]开始比对S[j],那么我们就可以不用从P的第0位比S的第1位,而是直接从P的P[k]开始比对S[j],这样j是不动的,动的只有模式串的i。当做到这就遇到了下一个需要解决的问题,怎么找k。也就是找i的下一个位置即next[i]。
next数组的求法。这个也可以用KMP算法的思想。首先第0位连前后缀都没有,所以我们规定next[0] = -1 。我们定义k是最长相同前缀后缀的前缀最后一位,j是后缀最后一位。那么起始k = -1 ; j = 0。如果P[k]==P[j] 那么k++,j++(即相同前后缀长度都加1)则P[0:j]中最长相同前后缀中前缀的最后一位就是k,所以next[j]=k。如果不相同,k就需要往前找。和KMP算法一样,这时候模式串就变成了P[0:k],文本串就变成了P[0:j]。那模式串中下一个与文本串匹配的位置不就是next[k]嘛。所以令k=next[k]。
二、代码
#include <iostream>
#include <vector>
using namespace std;
typedef struct
{
char data[MaxSize];
int length; //串长
} SqString;
//SqString 是串的数据结构
//typedef重命名结构体变量,可以用SqString t定义一个结构体。
void GetNext(SqString t,int next[]) //由模式串t求出next值
{
int j,k;
j=0;k=-1;
next[0]=-1;//第一个字符前无字符串,给值-1
while (j<t.length-1)
//因为next数组中j最大为t.length-1,而每一步next数组赋值都是在j++之后
//所以最后一次经过while循环时j为t.length-2
{
if (k==-1 || t.data[j]==t.data[k]) //k为-1或比较的字符相等时
{
j++;k++;
next[j]=k;
//对应字符匹配情况下,s与t指向同步后移
//通过字符串"aaaaab"求next数组过程想一下这一步的意义
//printf("(1) j=%d,k=%d,next[%d]=%d\n",j,k,j,k);
}
else
{
k=next[k];
//我们现在知道next[k]的值代表的是下标为k的字符前面的字符串最长相等前后缀的长度
//也表示该处字符不匹配时应该回溯到的字符的下标
//这个值给k后又进行while循环判断,此时t.data[k]即指最长相等前缀后一个字符**
//为什么要回退此处进行比较,我们往下接着看。其实原理和上面介绍的KMP原理差不多
//printf("(2) k=%d\n",k);
}
}
}
int KMPIndex(SqString s,SqString t) //KMP算法
{
int next[MaxSize],i=0,j=0;
GetNext(t,next);
while (i<s.length && j<t.length)
{
if (j==-1 || s.data[i]==t.data[j])
{
i++;j++; //i,j各增1
}
else j=next[j]; //i不变,j后退,现在知道为什么这样让子串回退了吧
}
if (j>=t.length)
return(i-t.length); //返回匹配模式串的首字符下标
else
return(-1); //返回不匹配标志
}
三、next数组优化
优化的地方就是,如果P[k]与S[j]不匹配,我们让k=next[k],P[k]如果等于P[next[k]],那么注定P[next[k]]仍然是不等于S[j]的。所以还需要找next[k]的next[next[k]]。所以我们在求next数组的时候,如果k++,j++后,那么next[j]=k,如果这时P[k]==P[j],也就代表着P[j]==P[next[j]]了,所以我们让next[j]=next[k]。就实现了往前倒了两层。
void GetNextval(SqString t,int nextval[])
//由模式串t求出nextval值
{
int j=0,k=-1;
nextval[0]=-1;
while (j<t.length)
{
if (k==-1 || t.data[j]==t.data[k])
{
j++;k++;
if (t.data[j]!=t.data[k])
//这里的t.data[k]是t.data[j]处字符不匹配而会回溯到的字符
//为什么?因为没有这处if判断的话,此处代码是next[j]=k;
//next[j]不就是t.data[j]不匹配时应该回溯到的字符位置嘛
nextval[j]=k;
else
nextval[j]=nextval[k];
//这一个代码含义是不是呼之欲出了?
//此时nextval[j]的值就是就是t.data[j]不匹配时应该回溯到的字符的nextval值
//用较为粗鄙语言表诉:即字符不匹配时回溯两层后对应的字符下标
}
else k=nextval[k];
}
}
int KMPIndex1(SqString s,SqString t)
//修正的KMP算法
//只是next换成了nextval
{
int nextval[MaxSize],i=0,j=0;
GetNextval(t,nextval);
while (i<s.length && j<t.length)
{
if (j==-1 || s.data[i]==t.data[j])
{
i++;j++;
}
else j=nextval[j];
}
if (j>=t.length)
return(i-t.length);
else
return(-1);
}