KMP 算法是一种改进的字符串匹配算法,由 D.E.Knuth , J.H.Morris 和 V.R.Pratt 提出的,因此人们称它为克努特 — 莫里斯 — 普拉特操作(简称 KMP 算法)。 KMP 算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个 next() 函数实现, 函数 本身包含了模式串的局部匹配信息。 KMP 算法的 时间复杂度 O(m+n) [1] 。 来自 ------- 百度百科。
在学习kmp算法之前我们需要理解BF算法的原理:
BF算法:
BF 算法,即暴力算法 ,是普通的模式匹配算法, BF 算法的思想就是将目标串 S 的第一个字符与模式串 T的第一个字符进行匹配,若相等,则继续比较 S 的第二个字符和 T 的第二个字符;若不相等,则比较 S 的第二个字符和T 的第一个字符,依次比较下去,直到得出最后的匹配结果。 BF 算法是一种蛮力算法
我们只需要定义一个变量i遍历主串,定义一个变量j遍历字串。暴力枚举即可
第一次匹配:
对应代码:
int strStr(string haystack, string needle) { int n=haystack.size(); int m=needle.size(); for(int i=0;i<=n-m;i++){//每次回退到前一个位置的下一个位置 bool flag=1; for(int j=0;j<m;j++){//j每次匹配失败后回退到0号位置 if(haystack[i+j]!=needle[j]){ flag=0; break; } } if(flag){ return i; } } return -1; }
i<=n-m的原因是当i走到主串的位置之后的字串的长度没有字串长了那么就不用找了因为长度都不一样所以i后面的字串肯定没有要找的字串了
时间复杂度:
设主串长度为n,模式串长度为m,假设主串从第 i 个位置与模式串匹配成功,则此前 i-1 趟字符总比较了 i-1 次,第i趟成功比较字符次数为m,则总比较次数为 i-1+m 次。
对于成功匹配的主串,其起始位置由 1 到 n-m+1,假定这 n-m+1 个起始位置上的匹配概率相等,则最好的情况下匹配成功的平均比较次数为:所以最好情况下平均时间复杂度是O(n+m)
最坏情况是每次匹配不成功发生在模式串最后一个字符,直达主串最后一个字符与之匹配。复杂度为: O(n*m)
下面让我们来看一下KMP算法:
假设有这两个串:
此时我们在红色阴影处匹配失败,绿色为匹配成功部分,如果按照BF算法那么就要将i下标回退到起始位置的后一个位置,j回退到起始位置,此时我们发现这并没有必要,因为回退之后i和j下标对应的值并不相等。
通过观察我们可以发现:这4个ab都是相等的,此时我们通过观察可以发现j回退到2号位置是最合适的,因为它最大程度的在主串中匹配字串,我们还可以发现j回退的位置的下标不就是ab的长度吗也就是p[0]....p[1]=p[3]...p[4],再来看i和j下标对应的值是否相同如果不同则j要继续回退
在这里我们就引出了next数组:
next数组的作用:保存字串某个位置失败后回退的位置
KMP 的精髓就是 next 数组:也就是用 next[j] = k;来表示,不同的 j 来对应一个 K 值, 这个 K 就是你将来要移动的 j要移动的位置。而 K 的值是这样求的:1、规则:找到匹配成功部分的两个相等的真子串(不包含本身),一个以下标 0 字符开始,另一个以 j-1 下标字符结尾。2、不管什么数据 next[0] = -1;next[1] = 0;在这里,我们以下标来开始,而说到的第几个第几个是从 1 开始;
求next数组的练习:
练习 1: 举例对于”ababcabcdabcde”, 求其的 next 数组?
到这里大家对如何求next数组应该问题不大了,接下来的问题就是,已知next[i] = k;怎么求next[i+1] = ?如果我们能够通过 next[i]的值,通过一系列转换得到 next[i+1]得值,那么我们就能够实现这部分。那该怎么做呢?首先假设: next[i] = k 成立,那么,就有这个式子成立: P0...Pk-1 = Px...Pi-1;得到: P0...Pk-1 = Pi-k..Pi-1;到这一步:我们再假设如果 Pk = Pi;我们可以得到 P0...Pk = Pi-k..Pi;那这个就是 next[i+1] = k+1;
在p[0]..p[k-1]=p[i-k]..p[i-1]的前提下又有p[i]=p[k]那么就有next[i+1]=k+1;
那么如果p[i]!=p[k]那么next[i+1]=?
此时p[k]!=p[i]那为什么next[i+1]=1了?这也意味着回退到的2号位置不一定是我们要找的数字,此时需要继续回退,k就回退到nxet[k]对应的位置如果仍然没有p[i]!=p[k]那么就一直回退直到p[k]=p[i]
下面我们来看一下代码:
int strStr(string haystack, string needle) { int len1=haystack.size(); int len2=needle.size(); if(len1==0&&len2!=0)return -1; if(len1==0&&len2==0)return 0; if(len1!=0&&len2==0)return 0; int i=0;//遍历主串 int j=0;//遍历字串 int *next=new int [len2]; Getnext(next,needle); while(i<len1&&j<len2){ if((j==-1)||haystack[i]==needle[j]){ i++; j++; } else{ j=next[j]; } } if(j>=len2) return i-j; else return-1; }
注意:如果一上来就匹配失败:那么j就会回退到-1的位置此时就会越界。所以我们要加这个特判,如果一上来就匹配失败j回到起始位置i继续往后走即可
对应next数组代码:
void Getnext(int *next,string&needle){ int len=needle.size(); next[0]=-1; if(len==1)return; next[1]=0; int k=0;//前一项的K int i=2;//此时i表示当前下标,也就是下一项 while(i<len){ if(k==-1||needle[i-1]==needle[k]){ next[i]=k+1; k++; i++; } else{ k=next[k];//此语句是这段代码最反人类的地方,如果你一下子就能看懂,那么请允许我称呼你一声大神! } } }
注意:1.如果字串的长度只有1我们需要特判一下防止next数组越界 .
2.i代表的是当前下标,也就是之前的i+1;
3.如果k回退到-1时需要特判
总代码:
class Solution { public: void Getnext(int *next,string&needle){ int len=needle.size(); next[0]=-1; if(len==1)return; next[1]=0; int k=0;//前一项的K int i=2;//此时i表示当前下标,也就是下一项 while(i<len){ if(k==-1||needle[i-1]==needle[k]){ next[i]=k+1; k++; i++; } else{ k=next[k]; } } } int strStr(string haystack, string needle) { int len1=haystack.size(); int len2=needle.size(); if(len1==0&&len2!=0)return -1; if(len1==0&&len2==0)return 0; if(len1!=0&&len2==0)return 0; int i=0;//遍历主串 int j=0;//遍历字串 int *next=new int [len2]; Getnext(next,needle); while(i<len1&&j<len2){ if((j==-1)||haystack[i]==needle[j]){ i++; j++; } else{ j=next[j]; } } if(j>=len2)//匹配成功 return i-j; else return-1;//匹配失败 } };
最后:🙌🙌🙌🙌
结语:对于个人来讲,在算法上进行探索以及单人闯关是一件有趣的时间,一个程序员,如果不喜欢编程,那么可能就失去了这份职业的乐趣。刷到我的文章的人,我希望你们可以驻足一小会,忙里偷闲的阅读一下我的文章,可能文章的内容对你来说很简单,(^▽^)不过文章中的每一个字都是我认真专注的见证!希望您看完之后,若是能帮到您,劳烦请您简单动动手指鼓励我,我必回报更大的付出~