KMP算法
一、什么是KMP
Knuth,Morris和Pratt,取了三位学者名字的首字母。 我何时有这么牛逼!!!哈哈!~~~
二、作用
主要思想:
当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去匹配了
本算法难点:
利用前缀表
记录已经匹配的文本内容(这个前缀表真的不好理解!!我花了老半天的时间。。。。)
1.
next数组
里的数字表示的是什么2.为什么这么表示?
三、前缀表是什么东东?
在这里先卖个关子:
next数组
我们在
看书
或者写算法题
时,都会看到答案的解释中都有next数组
或者prefix table
就是我们说的前缀表((⊙o⊙)…)
记录下标
i
之前的(包括i)的字符串中,有多大长度的相同
前缀和后缀
仔细揣摩
相同二字含义!!!
前缀表用来
回退
的,记录模式串
与文本串(主串)
在不匹配的时候,模式串应该从哪开始重新匹配
需求:
要在主串【aabaabaafa】中查找
是否出现一个模式串【aabaaf】
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lWD0F0TI-1654617955803)(Untitled.assets/KMP%E7%B2%BE%E8%AE%B21.gif)]
上图中的:
关键点
前缀表的aa
最长前缀匹配(就这一点我理解了半天!!)----后面我会说明如果采用暴力解法:当主串的第六个字符
b
和模式串的第六个字符f
不匹配时,就要重头来了
但是:
采用
前缀表
: 1.这玩意儿就不会重头开始,
2.而是从
上次
已经匹配
的内容开始匹配, 3.找到
模式串
中的中第三个字符b
继续开始匹配(是不是懵逼
了!!哈哈哈!!硬着头皮往后看)
**心中的疑问?**凭啥在模式串的第三个
字符开始匹配
前缀表
的任务:
当前的位置
匹配失败,找到之前已经匹配上的位置
在重新匹配,
就是某个字符失配时,
前缀表
会告诉你下一步匹配中,模式串
应该跳到哪个位置(怎么跳,再往下读)
我们说了:
再刚刚的匹配的过程,在
下标5
的大方遇到不匹配,模式串指向f
:如下图所示
![KMP精讲1](Untitled.assets/KMP%E7%B2%BE%E8%AE%B21.png)
然后就莫名其妙的指向了
下标2
(第三字符)
![KMP精讲2](Untitled.assets/KMP%E7%B2%BE%E8%AE%B22.png)
解释:划重点了哈!!
注意!(先看下)
**字符串的
前缀
**是指不包含最后一个字符
的所有以第一个字符开头
的连续子串
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-50Qzt6BB-1654617955804)(Untitled.assets/29950F6F.gif)]
下标5之前的这部分的字符串(就是
aabaa
)的最长相等的前缀
和后缀
字符串是子字符串aa
因为:找到了最长相等的
前缀
和后缀
,匹配失败
的位置是后缀子串
的后面,所以:我们找到与其相同的前缀的后面
重新
开始新匹配就可以了.(这句话好好咀嚼!!!精华)还是懵逼??哈哈~
- 你仔细向下模式串的前五个
aabbaa
与主串中前五个aabaa
是一样的对吧?- 这是我在
模式串
找最长前缀
与后缀
相等的数,是不是就是变相的在主串的子串aabaa
中找?(目的找到首次尾部能和头部相同的位置)- 这时
aa
在主串中位置是不是就可确定了???
注意!(再瞅瞅!!)
**字符串的
前缀
**是指不包含最后一个字符
的所有以第一个字符开头
的连续子串
四、前缀表的计算
如图:
长度为
1
的子字符串a
,最长相同前后缀的长度为0
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R9wHcA8O-1654617955805)(Untitled.assets/KMP%E7%B2%BE%E8%AE%B25.png)]
按照上面的方法寻找:(子字符串)
对应的最长相同前后缀长度对应的前缀表的元素
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5rHxPr8f-1654617955805)(Untitled.assets/KMP%E7%B2%BE%E8%AE%B28.png)]
下图利用前缀表找到当字符不匹配时应该指针移动到什么位置:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M9bBVbvQ-1654617955806)(Untitled.assets/KMP%E7%B2%BE%E8%AE%B22.gif)]
过程分析:
- 找到不匹配位置,我们要看他的前一个字符的前缀表数值是多少(这里是2)
- 找前缀表的前一个字符的原因:找到要找
前面字符串
的最长相同的前缀和后缀
五、前缀表与next
数组
其实就是一样的,只是具体的实现方式不一样罢了
next
数组:就是前缀表,只是很多实现将前缀表
进行统一的**减一(右移一位操作)**作为后面的next
数组
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6H88xp9F-1654617955807)(Untitled.assets/KMP%E7%B2%BE%E8%AE%B24.gif)]
六、时间复杂度----O(n+m)
O(n+m)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8z5NnoQ1-1654617955807)(Untitled.assets/29C539D2.png)]
七、构造next
数组
定义函数:getNext()
函数参数为指向next的数组的指针和一个字符串
void getNext(int* next,const string& s)
构造``next
数组就是计算
模式串`,过程步骤分析
1.初始化
//定义两个指针
//j指向前缀末尾位置,i指向后缀末尾位置,并对next数组初始化赋值
int j=-1;
next[0]=j;
说明:
j
初始化-1
的原因–前缀表的统一减一
操作,
next[i]
表示i
(包括i)之前的最长相等的前后缀长度
2.处理前后缀不相同的情况
//因为i=-1,右移一位操作,所以
i
从1
开始
for(int i=1;i<s.size();i++)
如果
s[i]
与s[j]
不相同,进行回退操作
next[j]
就是记录这``j`之间的子串相同前后缀的长度若
s[i]
与s[j+1]
不相同,找j+1
前一个元素在next
数组里的值(其实就是next[j])
//前后缀不相同
while(j>=0&&s[i]!=s[j+1])
{
//向前回退
j=next[j];
}
3.处理前后缀相同的情况
若
s[i]
yus[j+1]
相同,那马旧同事向后移动i
和j
说明找相同的前后缀,将``j`(前缀的长度)赋给next[i],记录相同的前后缀长度
//找到相同的前后缀
if(s[i]==s[j+1])
{
j++;
}
next[i]=j;
整合代码,万朝归元
void getNext(int* next,const string& s)
{
int j=-1;
next[0]=j;
//注意i从一开始
for(int i=1;i<s.size();i++){
//前后缀不同
while(j>=0&&s[i]!=s[j+1]){
//回退
j=next[j];
}
//z找到相同的前后缀
if(s[i]==s[j+1]){
j++;
}
//将j(前缀的长度)赋给next[i]
next[i]=j;
}
}
代构造的next
数组动画演示图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nGoByR9Z-1654617955807)(Untitled.assets/KMP%E7%B2%BE%E8%AE%B23.gif)]
主要实现代码(采用统一减一的操作!)
注意:看里面的注释!!
//前缀表统一减一操作
public void getNext(int[] next,String s){
//两个下标,j指向模式串needle的起始位置,i指向文本串haystack的起始位置
//j的初始值为-1,因为next数组中里面记录的起始位置为-1
int j=-1;
next[0]=j;
for(int i=1;i<s.length();i++){//注意当中=的i从1开始
while(j>=0&&s.charAt(i)!=s.charAt(j+1)){
j=next[j];//向前回退(是不是在想j=-1呢?看while里面的j>=0的条件)
}
if(s.charAt(i)==s.charAt(j+1)){
//找到相同的前后缀
j++;
}
//将j前缀的长度赋给next[j]
next[i]=j;
}
}
public int strStr_Sub1(String haystack,String needle){
if(needle.length()==0){
return 0;
}
int[] next=new int[needle.length()];
getNext(next,needle);
int j=-1;
//因为next数组里面记录的起始位置为-1
for(int i=0;i<haystack.length();i++){//注意这里i从0开始!!!(i指向的是文本串首位!!)
while(j>=0&&haystack.charAt(i)!=needle.charAt(j+1)){
//不匹配,寻找之前匹配的位置
j=next[j];
}
if(haystack.charAt(i)==needle.charAt(j+1)){//匹配,jhei同时向后移动
j++;//i的增加在for循环中
}
if(j==(needle.length()-1))//j到达模式串的末尾位置,说明文本串haystack出现了模式串needle
{
return (i-needle.length()+1);
}
}
return -1;
}