博主志在以通俗的语言加图片来带着大家一起理解kmp算法,如有疑问,随时联系博主呦!接下来我们一起学习吧
这时候我们的主串并不需要回溯,这个时候我们就只需要期待下标为10的‘?’是否等于G。如果不是,我们只需要i++,j依然指向1,然后判断j=1时是否与i++相同即可继续下一次查找。
倒数第二个字符不匹配的情况:
然而我们知道第五个元素不可能是我们要查找的串的首元素,同理6、7也是,当到了八的时候,又出现了可能查找成功的情况,如下:
其他情况
同样的第一,第二个位置不匹配也类似上述情况,我们将返回值列一个表格可得:
若当前两个字符匹配,则i++,j++
若 j=1 时发生不匹配,则应让 i++,而j依然是1
若j=2时发生不匹配,则应让j回到1
若j=3时发生不匹配,则应让j回到1
若j=4时发生不匹配,则应让j回到1
若j=5时发生不匹配,则应让j回到2
若j=6时发生不匹配,则应让j回到1
我们用next数组来保存他**(考虑数组下标是从0开始)**:int next[6]={-1,0,0,0,1,0};
我们来看具体实现过程:
int Index_KMP(SqString S, SqString T, int next[])
{
int i = 0, j = 0;
while (i < S.length && j < T.length)
{
if (j == -1 || S.data[i] == T.data[j])
{
i++;
j++;
}
else
j = next[j];
}
if (j >= T.length)
return (i - T.length);
else
return -1;
}
我们在匹配不成功时,让j=next[j
];即可,当然上述next数组只是针对于我们上述特定的字符串来实现的,而next数组不是一成不变的,打起精神,我们继续看下列内容:
这时候问题来了,我们要实现在主串查找子串的功能又该怎么做呢?毕竟我们只是分析了上述查找“GOOGLE”这个子串,换个子串又得怎么做呢?如何去获得属于他的next数组呢?接下来我们去分析next数组如何求取。
首先我们看abcabd这种情况:
**这时候我们让next[6]=3;
在看:我们可以看到向后移两个便可继续匹配,然而如图,如果我们向后跳四个,也是可以匹配的,那我们可以取向后跳四个的情况吗?
我们可以看到,在第三个元素我们就找到了我们需要的结果,如果向后移四个,且不说可不可以找到,找到了他也不是我们需要的最准确的结果,所以我们是不是应该先寻找匹配到的相同元素最大的,或者可以说最先找到的呢?我们有以下概念:
串的前缀:包含字符串的第一个字符,不包含最后一个字符的子串;
串的后缀:包含字符串的最后一个字符,不包含第一个字符的子串。
例如:
字符串 abcdab
前缀的集合:{a,ab,abc,abcd,abcda}
后缀的集合:{b,ab,dab,cdab,bcdab}
当第j个字符匹配失败,则前1~j-1个元素记作S,其中next[j]=S最长相等前后缀+1
;那这样求next数组就变得很轻松了,我们也可以考虑用代码实现。我们总结求next公式如下图:
下面的长条代表子串,红色部分代表当前匹配上的最长相等前后缀,蓝色部分代表t.data[j]。
根据这些内容,我们做出如下代码:
typedef struct
{
char data[MaxSize];
int length; //串长
} SqString;
//typedef重命名结构体变量,用SqString T定义一个结构体。
void GetNext(SqString T, int 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数组过程想一下这一步的意义
}
else
{
k = next[k];
//我们现在知道next[k]的值代表的是该处字符不匹配时应该回溯到的字符的下标
//这个值给k后又进行while循环判断,此时t.data[k]即指最长相等前缀后一个字符
}
}
}
现在我们分析一下KMP算法的时间复杂度:KMP算法中多了一个求数组的过程,多消耗了一些空间。我们知道KMP是以空间换时间的算法,我们设主串S长度为n,子串T的长度为m。求next数组时时间复杂度为O(m),因后面匹配中主串不回溯,比较次数可记为n,所以KMP算法的总时间复杂度为O(m+n),空间复杂度记为O(m)。相比于朴素的模式匹配时间(BF算法)复杂度O(m*n),KMP算法提速是非常大的,这一点点空间消耗换得极高的时间提速是非常有意义的,这种思想也是很重要的。
int Index_KMP(SqString S, String T)
{
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++;
}
else
j = next[j]; //i不变,j后退到next标记的下标处。
}
if (j >= T.length)
return(i - T.length); //返回匹配模式串的首字符下标
else
return(-1); //返回不匹配标志,表示查找失败
}
然而,我们的KMP还是可以优化的,那么他已经这么厉害了为什么还需要优化呢?
主串s=“aaabaaaaac”
子串t=“aaac”
这个例子中当‘b’与‘c’不匹配时我们知道没有必要再将‘b’与‘a’比对了,因为回溯后的字符和原字符是相同的,原字符不匹配,回溯后的字符自然不可能匹配。但是KMP算法中依然会将‘b’与回溯到的‘a’进行比对。这就是我们可以改进的地方了。
那我们应该咋做呢?
其实我们可以简述为: 如果a位字符与它next值指向的b位字符相等,则该a位的nextval就指向b位的nextval值,如果不等,则该a位的nextval值就是它自己a位的next值。
这应该算是比较通俗的解释了!
我们将求next数组的代码修改为:
void GetNextval(SqString T, int 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])
nextval[j] = k;
else
nextval[j] = nextval[k];
//此时nextval[j]的值就是就是T.data[j]不匹配时应该回溯到的字符的nextval值
}
else k = nextval[k];
}
}
kmp算法的核心在于求解next(nextval)数组,博主也是苦于kmp久矣,今日顿悟,也是愈发感觉到计算机前辈的厉害之处,大家一定不要放弃kmp,仔细品味,终究会彻悟的!加油!
如果博主的文章给你提供了帮助,给博主一个不要钱的赞可好?对博主来说可美可香了!