在讲这两个算法之前,需要先了解什么是模式匹配?
模式匹配的定义:
设有主串S和子串T,子串T定位是指在主串S中找到一个与子串T相等的子串。通常把主串S称为目标串,把子串t称为模式串,因此定位也称为模式匹配。匹配成功,是指在目标串S中有一个子串等于模式串T;匹配失败,是指目标串S不存在子串等于模式串T。
举一个简单一点的例子吧,在一篇文章中,你想要找到一句话在文章中是否出现,如果出现,就是匹配成功,如果没有出现就表示匹配失败!
朴素的模式匹配算法
朴素的模式匹配算法比较好理解,朴素的模式匹配算法是将字串与主串一一进行比较。
上述图片就表示了朴素的模式匹配算法的过程。从主串的第一个元素开始对比,若不与子串相同,则向后移动一位继续对比,一直对比到模式串的最后一位于主串最后一位对齐,若中间没有与子串匹配,则匹配失败,若有与之匹配的,则匹配成功,返回匹配的位置。
接下来我们来看代码:
/*返回子串T在主串中的位置,若不存在,则返回0.*/
/*T[0] 和 S[0] 表示字符串的长度*/
int Index(string T,string S)
{
int i = 1; //i表示主串S的下标
int j = 1; //j表示子串T的下标
while(i <= s[0] && j <= t[0]){ //若i小于S长度且j小于T的长度进行循环
if(S[i] == T[j]){ //两个字符若相等,则继续
++i;
++j;
}
else{ //若不想等,则回退,i退为上一次的下一个,j退为1
i = i - j + 2;
j = 1;
}
}
if(j > T[0]) return i - T[0];
else return 0;
}
这就是朴素的模式匹配算法的代码。
这虽然能够找出主串中与子串相匹配的,但它的时间复杂度是非常高的,举一个例子:
若主串为S = 000000000000000000000000000001
字串为T = 000001
这个例子若用上述方法,它就需要主串前面24个元素每一个都与子串中的元素相比较,非常的浪费时间。
KMP模式匹配算法
继续来看这个例子:
在匹配第一次之后,我们发现,子串中第一位g与主串中后面的几位均不相同,则可以直接跳到第5步执行。如此,我们的步骤将会非常简单,我们也省略了许多不必要的步骤。
我们来再看一个例子:
在这个例子中,我们也发现2、3、4、5步都是可以省略的,所以我们可以只进行第1、6步。
接着来看下一个例子:
在这个例子中,我们发现,第二三步就可以省略。我们可以直接进行第四步。通过上面两个例子发现,我们要省略的步骤其实与主串没关系,和子串前后缀的相似程度有关系,所以我们把子串拿出来单独研究。
在abcdex这个例子中,当 j 等于6时,x与主串不匹配,j 变为了1,在abcabx这个例子中,我们的x与主串不匹配,j 从6变为了3。
我们把子串各个位置的 j 值变化定义为一个数组,将其称为next数组。
来看一下这个例子:
再来看这个例子:
3.T=“ababaaaba”
在上述例子中我们已经大致了解了next数组的推导方式,接下来我们来看一下next数组的代码实现:
/*
next 数组
T[0] 表示子串的长度
*/
void get_next(string T,int *next)
{
int i ,j;
i = 1;
j = 0;
next[1] = 0;
while(i < T[0]){
if(!j || T[i] == T[j]){ //T[i] 表示后缀单个字符
++i; //T[j] 表示前缀单个字符
++j;
next[i] = j;
}
else{
j = next[j]; //若字符不同,则j 回溯
}
}
}
以上是next数组推导方式的代码。
讲了这么多,什么是KMP算法呢?
KMP算法就是避免重复遍历的一种算法。
接下来我们将要用next数组来实现我们更加简便的算法了。
//返回子串T在主串中的位置,若不存在,返回0
//T非空
int Index_KMP(string S,string T)
{
int i = 1;
int j = 1;
int next[strlen(T)];
get_next(T,next) ;
while(i < S[0] && j <= T[0]){ //若i小于S长度且j小于T的长度时循环
if(j == 0 || S[i] == S[j]){ //两字符相等则继续
++i;
++j;
}
else{
j = next[j]; //j 退回合适的地方
}
}
if(j > T[0]){
return i - T[0];
}
else{
return 0;
}
}
以上就是KMP算法的实现了,但是KMP还是有一定的缺陷,比如下面这个例子:
在这个例子中,我们发现其实2、3、4、5步是多余的判断,我们发现,T串前5个位置的值是相同的,我们可以用首位next[1]的值去取代与他相等字符后续next[j]的值,所以我们可以这样改良我们的next数组:
/*
nextval 数组
T[0] 表示子串的长度
*/
void get_nextval(string T,int *nextval)
{
int i ,j;
i = 1;
j = 0;
nextval[1] = 0;
while(i < T[0]){
if(!j || T[i] == T[j]){ //T[i+1] 表示后缀单个字符
++i; //T[j] 表示前缀单个字符
++j;
if(T[i] != T[j]) nextval[i] = j; //如果与前缀字符不同,j 为 nextval 在 i 位置的值
else nextval[i] = nextval[j]; //如果与前缀字符相同,则为前缀
}
else{
j = nextval[j]; //若字符不同,则j 回溯
}
}
}
我们将新的数组叫做nextval数组。
nextval数组的推导:
当我们计算出next数组后,我们计算nextval数组,当nextval数组的第 i 位与其next数组所指向的第 j 位相同,则第 i 位的nextval值为第 j 位next的值,如果不相同,则第 i 位nextval的值为next的值。
总结
我刚开始学习KMP算法,对其理解也不是特别深刻,如若有什么说的不对的,希望各位能够指正,我不胜感激。