今天学习kmp算法,看了好多大佬的文章,总算是解决了理解层面上的问题,现在和大家分享一下。
KMP算法是一种改进的字符串匹配算法
主串 为:a b a c a a b a c a b a c
模式串 为:a b a c a b
目的就是为了找出模式串在主串中的起始位置。
a b a c a a b a c a b a c 此时起始位置是6。
我们就是为了确定这个位置
当然是为了更快找到这个位置,kmp的算法复杂度比较低,是一种优秀的算法。
一般匹配字符串时,我们从目标字符串str(假设长度为n)的第一个下标选取和ptr长度(长度为m)一样的子字符串进行比较,如果一样,就返回开始处的下标值,不一样,选取str下一个下标,同样选取长度为n的字符串进行比较,直到str的末尾(实际比较时,下标移动到n-m)。这样的时间复杂度是O(n*m)。
但是KMP算法:可以实现复杂度为O(m+n)!怎么样,是不是快了很多。
主要就是因为它充分利用了目标字符串ptr的性质(比如里面部分字符串的重复性,即使不存在重复字段,在比较时,实现最大的移动量)。
使用kmp算法有两个步骤
next数组的含义就是一个固定字符串的最长前缀和最长后缀相同的长度。
比如:
注意最长前缀:是说以第一个字符开始,但是不包含最后一个字符。
比如aaaa相同的最长前缀和最长后缀是aaa。
对于目标字符串ptr,ababaca,长度是7
位置 | next[0] | next[1] | next[2] | next[3] | next[4] | next[5] | next[6] |
---|---|---|---|---|---|---|---|
部分字符串 | a | ab | aba | abab | ababa | ababc | ababca |
长度 | -1 | -1 | 0 | 1 | 2 | -1 | 0 |
(-1表示不存在,0表示长度为1,1表示长度为2,这样设置的原因是代码实现,写代码的时候方便)
下面是求next数组的代码实现
void cal_next(char *str, int *next, int len) {
next[0] = -1;//next[0]初始化为-1,-1表示不存在相同的最大前缀和最大后缀
int k = -1;//k初始化为-1
for (int q = 1; q <= len-1; q++) {
while (k > -1 && str[k + 1] != str[q]) {//如果下一个不同,那么k就变成next[k]
k = next[k];//往前回溯
}
if (str[k + 1] == str[q]){//如果相同,k++
k = k + 1;
}
next[q] = k;//这个是把算的k的值(就是相同的最大前缀和最大后缀长)赋给next[q]
}
}
大家第一次看这个代码肯定一头雾水,稳住不要慌,相信自己肯定能弄懂!
首先函数声明,初始化,因为next[0]对应的就是"a",因为最长前缀以第一个字符开始,但是不包含最后一个字符,所以"a"没有最长前缀和最长后缀,因此对应的长度就是-1。
而后有一个for循环,就是遍历next数组。
再看for循环里面
while (k > -1 && str[k + 1] != str[q]) {
k = next[k];
}
if (str[k + 1] == str[q]){
k = k + 1;
}
首先因为k==-1,所以跳过while循环,又因为str[k + 1] != str[q],因此也不进入if语句,最后next[1]=-1。
之后for循环q++,因为此时str[k + 1] == str[q],因此k++,最后next[2]=0,代表"aba"的最长前缀和最长后缀的相同长度为1。
之后for循环q++,因为此时str[k + 1] == str[q],因此k++,最后next[3]=1
之后for循环q++,因为此时str[k + 1] == str[q],因此k++,最后next[4]=2
之后for循环q++,因为此时str[k + 1] != str[q]
因此我们进入while循环
k=next[k]
我们称之为回溯,如何理解呢
我们现在面临的问题是,长度为4(k=3)的最长前缀和最长后缀不匹配,原因是末尾的str[k + 1] != str[q]
那么我们的解决办法是什么呢?
一般的思路:既然长度为4匹配不成功,那我们匹配长度为3不就行了?要是长度为3不行,再匹配长度为2。。。依次这样进行下去。
但是我们发现,长度为3的匹配出现了问题,不仅仅是末尾不匹配,连开头也不匹配了。我们不能依靠“只判断最后一位是否相同”这个条件来判断匹配是否成功了。
这样算法就会变得很复杂,就算最后一位相同了,我们还是要判断整体是否相同,变得更加麻烦了。
因此我们要确定正确的位置:
保证下一步匹配的前缀和后缀,只有最后一位不相同。
通过观察我们发现,虽然str[k + 1] != str[q],
但我们知道str[k + 1] 之前的序列,它的最长前缀和最长后缀相同的长度是已知的,例如
在c之前的序列,k=2(长度为3),那我们就可以用这个之前的序列。
我们发现这就是前面我们已经做过的步骤,
那我们要接着回溯下去,找这个最长前缀中的最长前缀,最长后缀中的最长后缀,也就是
也就是k=next[k],回溯
因为k=2,next[2]=0,因此k=0
此时str[k + 1]='b’和 str[q]=‘c’,发现str[k + 1] != str[q]
再次while循环,
k=next[k],回溯
因为k=2,next[0]=-1,因此k=-1
放大看一下
此时k=-1,跳过while循环,
又因为str[k + 1] != str[q]
因此next[5]=-1。
而后q++,因为str[k + 1] == str[q],因此k++,最后next[6]=0
循环结束,next数组创建完成。
以上大家应该能够理解next数组的代码思路了吧
之后的算法其实和求next数组差不多,本来想写一下,发现这篇文章写的很好,完整算法看这位大佬的就好了。
。。。思考了一下,还是自己再梳理一遍,也不是很难
首先这里默认已经求出了next数组
贴上代码
int KMP(char *str, int slen, char *ptr, int plen)
{
int *next = new int[plen];
cal_next(ptr, next, plen);//计算next数组
int k = -1;
for (int i = 0; i < slen; i++)
{
while (k >-1&& ptr[k + 1] != str[i])//ptr和str不匹配,且k>-1(表示ptr和str有部分匹配
k = next[k];//往前回溯
if (ptr[k + 1] == str[i])
k = k + 1;
if (k == plen-1)//说明k移动到ptr的最末端,匹配成功了!
{
return i-plen+1;//返回相应的位置
}
}
return -1; //匹配失败了,没有匹配到合适的
}
首先第一眼看这个代码,感觉和next数组的求法大同小异。
str是母串,ptr是子串。
主要就是从这个for循环开始,我们要明确k和i的含义
所以刚开始一个都没有匹配的时候,k的值是不变的,但是i的值却在增加,图解说明一下
i=0,k=-1
a | a | a | b | c | b | d |
---|---|---|---|---|---|---|
b | c | b | s |
i=1,k=-1
a | a | a | b | c | b | d |
---|---|---|---|---|---|---|
b | c | b | s |
i=2,k=-1
a | a | a | b | c | b | d |
---|---|---|---|---|---|---|
b | c | b | s |
i=3,此时匹配到了一个,进入if结构,最后k=0
a | a | a | b | c | b | d |
---|---|---|---|---|---|---|
b | c | b | s |
i=4,此时匹配到了2个,k=1
a | a | a | b | c | b | d |
---|---|---|---|---|---|---|
b | c | b | s |
i=5,此时匹配到了3个,k=2
a | a | a | b | c | b | d |
---|---|---|---|---|---|---|
b | c | b | s |
i=6,这个时候出现了不匹配的情况
执行k=next[k],k=next[2],因为bcbs
的next[2]值是0(一个),所以k=0,i=6图解一下就是
a | a | a | b | c | b | d |
---|---|---|---|---|---|---|
b | c | b | s |
转变为
a | a | a | b | c | b | d | |||
---|---|---|---|---|---|---|---|---|---|
. | . | b | c | b | s |
仍处于while循环中,还是没有匹配,所以k=next[0]
因此k=-1,q=6
a | a | a | b | c | b | d | |||
---|---|---|---|---|---|---|---|---|---|
. | . | . | b | c | b | s |
还是没有匹配到,此时k=-1跳出循环,所以匹配结束,返回-1。
经过这样的梳理,大家应该对这个kmp算法完全理解了吧,欢迎在评论区进行讨论,共同学习进步。