C数据结构:KMP算法详解(呕心沥血)
KMP算法
作者心声
(* ̄︶ ̄)
首先希望各位不要被KMP算法吓到,理解之后其实很容易上手。
这篇博客的目的是希望通过鄙人的方式写出一些通俗易懂的“ 妙手 ”想法去理解,所以在观看本篇博客前请三思~
因为网上对于KMP算法的解释很多,比我解释的还准确,所以如果你想理解更深更透彻的话可以搜索KMP算法,找到浏览次数最高的即可,若你只是想知道或者只是想解决掉KMP算法是如何执行和认识该算法的运行过程或许我这篇博客能帮到你。
我尽可能的把我在学习过程中遇到的困难和不理解的地方都写在这篇博客上,所以初衷还是希望用自己的理解方式来帮助大家理解KMP算法。
(说不定我之前遇到的问题就是你一直不理解的难点呢~)
了解暴力求解(必需会)
假设我们有一段特别长的文本内容称之为主串,里面全是字母,现我给你一个模式串,要求你在主串中找到这个模式串在主串中的位置。
举个栗子:主串:a p a c a p a c k p a c c
要找的模式串为: p a c k p a c
a p a c a p a c k p a c c | 主串 |
---|---|
^ ^ ^ ^^ p a c k p a c | 模式串首字母 p 在主串的位置是:6 |
我们肉眼很快就能找到该单词所在位置,
(在主串数组下标就是5)
按照 p a c k p a c z 中的首字母 p 出现的位置作为该 p a c k p a c z 在主串中的位置是第6个
我们在学习KMP算法之前必须对暴力求解的方法有一个了解。
#什么是暴力求解#:
i 为主串的下标
j 为模式串的下标
假设现在从主串的第一个字符开始和模式串匹配
所以 i 和 j 下标为0 (数组中0是首元素)
(i 和 j 现在都是0) i 和 j 在遍历的时候,当 主串 中一个字符和模式串中的不一样,i 就立马 回溯到 i + 1 的位置重新开始和模式串匹配,
然而 模式串 只要匹配错误就立马 j = 0,直接返回数组首元素位置。
再次拿上面的例子解释:
| a p a c a p a c k p a c z | ----------主串
| p a c k p a c z | ------------------模式串
现在 i 和 j 都为 0 下标,一开始 0 就不匹配了,
所以 i 回溯到 0 + 1 下标的位置重新开始匹配
j 在每一次匹配失败的都变为 0 也是重头开始匹配
然后如果又匹配错误,i 回溯到2 ,然后 j 又重新变为0。
以此类推下去,最坏的情况就是 i 一直回溯
@@不难推出,两个串要匹配成功 i 需要回溯 3 次@@
(可以自己试着推一下)
注:虽然该暴力解法很easy很合情合理,但是当这个主串非常大,甚至不局限与字符串而是要应用在大量数据的时候就显得非常鸡肋,因为匹配的时间实在是太长了。
OK现在开始学习KMP算法了,上面的暴力求解应该理解了吧?
( 没能理解恕鄙人无能为力…… )
如果理解了,那么恭喜你,成功把KMP算法理解了三分之一了,因为这个和KMP算法有着异曲同工之处。
KMP算法详解
记住我这段话(你会爱上它的)← :
KMP算法其实就是主串一直i++下去,不用回溯
,然后模式串回溯的位置变成看前后缀相同的个数作为回溯的下标位置,
比如前后缀有一个相同的,就回溯到1的位置,
abcas,假如到了abca的时候下一个s匹配错误,
那么这时候可以看到s前面的前后缀有一个相同的
这时候模式串回溯到b的位置,下标为1,
也就是相同前后缀的个数作为下标进行回溯。
如此一来,KMP算法就大大降低了回溯的时间复杂度,因为i不再需要回溯了,
而模式串只需要根据前后缀进行回溯,也不用直接回溯到 j = 0.
(请务必带着我这段话去看下面解释,看完看继续回头看这一段!!)
①前后缀及其用处
前缀和后缀
前缀:不包括字符串尾字母
后缀:不包括字符串为首字母
有一串字符串↓
例: abcd
前缀:a ab abc
后缀:bcd cd d
## 这里有一个 混淆 点 ##:
我们KMP中的前后缀其实质是给模式串服务的,我看其他博客解释都有带上主串,我觉得太累赘了,其实你想想,当你模式串中某一部分匹配成功的时候,其实主串已经和你的模式串有一部分是一样的了,我们只需要知道前后缀给模式串服务就行,不需要附带上主串给自己的大脑增添没有必要的负担。
(这个混淆看不懂没关系,坚持住往后看就会恍然大悟)
接着再次拿上面的例子解释:
主串 i 回溯第一次
| a p a c k p a c k p a c z | ----------主串
| ^ p a c k p a c z | ------------------模式串
这时候可以看到模式串的前后缀有三个相同的字母,
还记得我让你记住的那一段话吗,有三个相同前后缀的字母应该要怎样?
答:应该是要把模式串下标 j 回溯到下标为3的位置继续和主串 i 的位置继续比较,这里回溯的第一次 ,i 是处于 k 的这个位置,所以当模式串回溯到 3 的位置 也就是 pack中 k 的这位置开始和主串继续匹配,很明显下面我们就能和主串匹配成功了
想象一下,如果我们用暴力求解, 在回溯第一次后,我们下一步肯定是把 i 回溯到第二位 i = 2 的位置,因为我们是从i = 1 的位置匹配的,这个位置开始匹配不行,暴力方法就很无脑的把 i = 2 从第三个位置 a 重新开始匹配, 然后模式串 j = 0。这样一来,如果是暴力方法的话,我们还需要4次才能真正的匹配成功,而刚刚KMP算法直接用一步到胃,省了的时间可不止一半了喔~
不知道你理解了没有,如果还没理解的话我真的没法子了。
下面开始介绍前后缀怎么求
②求出前后缀的next数组
还记得我们说过前后缀是为谁服务的吗?可以思考一下~
答案是
*
*
*
*
前后缀当然是为模式串服务的啦。
所以我们需要一个做一个函数,把模式串放进去,将一个int类型的next数组放进去
该数组是用来存放模式串对应位置前面的字符串的相同前后缀个数
( ↑ 请再读一遍这句话)
因此该数组每一个下标对应存放的信息也是和模式串下标每一个字符对应服务的
在这个函数中,我们只需要两个参数,一个数组,一个模式串
下面开始举例子理解next数组
a b c g a k
现在 k 对应的字符串下标为 5
所以next数组在对应着下标为5的空间下存放一个数 1,
因为现在这个字符所处的位置的前面的字符串只有一个相同的前后缀
再出一题昂,看看你做的对不对
ilovewzlilove y yds
问:第一个y所对应的next数组存放的数字应该是?
/
/
/
/
/
/
/
/
/
/
答案是:5
因为i love 是五个字母 (额…好土)
好的那么恭喜你成功理解了next数组是怎么来的了,
所以模式串中每一个字符你都应该可以推出他该字符所对应的next数组的数字了
但是别忘记了,前缀不包括最后一个字符,后缀不包括第一个字符
所以这也是一个难点,你记住,每一个算法都有他的边界,这也是算法难的原因,边界掌握到位了,你也能够出师了。
所以在next数组里面,next[0]所存的数字应该是-1,为什么呢,其实这个next[0] == -1 可以作为一个判断条件,待会你就知道为什么了。当一直处于匹配错误的时候,就拿他卡主模式串,当前后缀个数为0 的时候 j 就直接等于next[0],也就是j = -1,但是别怕,数组没有-1 的下标,但是还记得吗,刚刚才说了,这个next[0] 为 -1 可以作为一个判断条件 让主串不断前进,模式串回到0的下标位置匹配,别想太多,因为前后缀都没有一样的了,只能重新搞啦!懂我意思嘛!
求出next数组的代码
假设模式串是 :a a k f
这串代码我带你口述运行四遍,然后那你再看那个代码 (自己循环四遍就能看出规律)
首先模式串有两个哨兵 i, j
int i = 0, j = -1;
next[0] = -1;
// 这里的 i 是要完成他 ++ 的使命就OK, 接下去运行你就知道了。
if( j == -1 || temp[i] == temp[j] )
{
i++;
j++;
next[i] = j;
}
else
{
j = next[j];
}
运行第一遍 模式串是 :a a k f
进来先判断if
显然if在 j == -1 成立
所以进来后 i++ , j++(i=1,j = 0)
next[1] = 0
循环第二遍 模式串是 :a a k f
j 显然现在是0,所以判断第二个条件,
现在我们的模式串 0和 1下标字符相等,那么现在进入if语句,条件成立
所以继续 i++, j++, 现在 i = 2, j = 1
next[2] = 1;是不是很精髓,这语句就是先把下标自加了,
就是说第三个位置的前面有一个相等的前后缀,因为下标2就是第三个字符位置
所以先自加完后再给next数组就就很牛蛙,
循环第三遍 模式串是 :a a k f
现在帮你回忆下第二遍循环的变量情况:i = 2, j = 1
然后继续循环,回到if语句了,这惠很显然
temp[i]不等于temp[j]而且j 也不是-1,
因为第三个和第二个字符不一样,所以这次跳到else语句那里
j = next[j];//KMP算法精髓之处,现在next[j]是0因为先是j = 1,然后再被赋值为0
循环第四遍 模式串是 :a a k f
显然,这时候j 不是-1,然后继续匹配还是停留在i = 2, j = 1 的时候,显然这两个字符不相等,也没有进到if语句里面,
而是进去else语句里面,这时候j = 0, 所以next[j] = -1, j 被重新赋值为 -1,妙不妙就问你,前辈是多么的diao炸天,不仅控制了 i 不继续前进,而是先把 j 先回溯到原来的位置,然后这时候
第五遍的时候你应该自己可以推了,
这时候, j = = -1 了,这时候就可以进入 if语句了,重复上述操作,就先了解到这,如果你这能看懂,我觉得你可以继续推下去,更能理解深刻,我这只是帮你开开窍。
void Getnext(int *next, char *temp)
{
int i = 0, j = -1;
next[0] = -1;
while(i <= strlen(temp))
{
if(j == -1 || temp[i] == temp[j])
{
i++;
j++;
next[i] = j;
}
else
{
j = next[j];
}
}
}
开始实现KMP算法
铺垫这么多,下面就很容易了,因为KMP算法思想和求next数组思想差不多,甚至代码也长得很像,
记住一句话,前后缀有相同的时候,模式串的前缀不用匹配,因为主串中有与你的前缀相同的部分,直接移动到那一部分的位置就行,next数组就是为你的模式串干这事的。
需要注意的是
i < l_str && j < l_t
这个大于小于符号不能取等号,严格大于小于,没有等于
因为strlen计算长的的时候是直接给出字符串长度,但是我们的下标是从0开始的,
否则会造成下标越界,
(应该没有人特地去把strlen计算出来的长度减一然后条件就取等于吧,虽然这是可以,但是很傻)
还记得我让你记住的那一段话吗,你再回去读一遍理解一下,然后再看我这段代码,我就不信你妹有爱上那句话!!( 虚弱的说… )
↓ 下面的时间就留给你好好去欣赏一下前辈的算法艺术 ↓
void KMP(char *string, char *temp)
{
int next[10];
int i = 0, j = 0;
Getnext(next, temp);
//
// for(i = 0; i < strlen(temp); i++)
// {
// printf("%d\t",next[i]);
// }
// i = 0, j = 0;
int l_str = strlen(string), l_t = strlen(temp);
while(i < l_str && j < l_t)
{
if(j == -1 || string[i]==temp[j] )
{
i++;j++;
}
else
{
j = next[j];
//printf("**%d\t",j);
}
}
int c = strlen(temp);
//可能是编译器的问题,如果我不拿个c存放这个长度,直接反倒条件判断里面运行不出来
if( j >= c )
{
printf("\n%d位置:找到了。",i-strlen(temp) + 1);
return;
}
else
{
printf("\n没有找到匹配的部分。");
return;
}
}
结尾
虽然很垃圾的一篇博客,KMP算法确实难以理解,同样也难以解释
费时几天时间才把博客写出来
全部都是鄙人的习道,如果能帮到你,我将会很高兴,
如果没能把帮到你,恕我无能。
感谢你能看到这~
附上源代码(可以运行)
#include<stdio.h>
#include<string.h>
void Getnext(int *, char *);
void KMP(char *, char *);
int main()
{
char string[11] = "chinadaily";
char temp[11];
int next[10];
printf("\n请输入字符串进行匹配:");
scanf("%s", temp);
KMP(string, temp);
return 0;
}
void Getnext(int *next, char *temp)
{
int i = 0, j = -1;
next[0] = -1;
while(i <= strlen(temp))
{
if(j == -1 || temp[i] == temp[j])
{
i++;j++;
next[i] = j;
}
else
{
j = next[j];
}
}
}
void KMP(char *string, char *temp)
{
int next[10];
int i = 0, j = 0;
Getnext(next, temp);
//
// for(i = 0; i < strlen(temp); i++)
// {
// printf("%d\t",next[i]);
// }
// i = 0, j = 0;
int l_str = strlen(string), l_t = strlen(temp);
while(i < l_str && j < l_t)
{
if(j == -1 || string[i]==temp[j] )
{
i++;j++;
}
else
{
j = next[j];
//printf("**%d\t",j);
}
}
int c = strlen(temp);
//可能是编译器的问题,如果我不拿个c存放这个长度,直接反倒条件判断里面运行不出来
if( j >= c )
{
printf("\n%d位置:找到了。",i-strlen(temp) + 1);
return;
}
else
{
printf("\n没有找到匹配的部分。");
return;
}
}
本文来自博客园,作者:竹等寒,转载请注明原文链接。