KMP算法
什么是KMP算法?在字符串中查找指定的子串并返回位置,找不到返回-1
注:代码中部分代码是伪代码
算法过渡:
在下面的算法中我们都以字符串ABABCD为例
一.朴素模式匹配算法(KMP算法的由来)
char c[7]={‘ ’ ,‘A’,’B’,’A’,’B’,’C’,’D’}
我们拿子串ABC去匹配,看看过程是怎样的:
第一轮匹配(指针从1往后移动):
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | |
---|---|---|---|---|---|---|
主串 | A | B | A | B | C | D |
待匹配串 | A | B | C |
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | |
---|---|---|---|---|---|---|
主串 | A | B | A | B | C | D |
待匹配串 | A | B | C |
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | |
---|---|---|---|---|---|---|
主串 | A | B | A | B | C | D |
待匹配串 | A | B | C |
由于c[3]匹配不上,进行第二轮:
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | |
---|---|---|---|---|---|---|
主串 | A | B | A | B | C | D |
待匹配串 | A | B | C |
由于c[2]匹配不上,进行第三轮:
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | |
---|---|---|---|---|---|---|
主串 | A | B | A | B | C | D |
待匹配串 | A | B | C |
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | |
---|---|---|---|---|---|---|
主串 | A | B | A | B | C | D |
待匹配串 | A | B | C |
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | |
---|---|---|---|---|---|---|
主串 | A | B | A | B | C | D |
待匹配串 | A | B | C |
匹配上了,算法结束!
综述:可以看出朴素模式匹配算法是从主串ABABCD的第一个位置开始往后匹配,如果发现后面匹配不上,就把主串的指针挪到下一个位置继续从子串的头部重新开始匹配。
引出:如果遇到主串aaaaaaaaaaa,拿着子串aaab去匹配,就会造成主串的指针频繁的回溯,效率低下,所以K、M 和 P 这三个人就研究出来一种算法,使得主串指针不用回溯也可以达到相同的目的,这就是著名的KMP算法!
二、KMP算法
我们说主串的指针不用回溯,究竟是怎样一回事呢?来看看:
先上代码:
// 用S代表主串,P代表待匹配的子串
int KMP(S, P)
{
int i=1, j=1;
while(i<=S.length && j<=P.length)
{
if(j==0 || S[i]==P[j])
{
i++;
j++;
}
else
{
j=next[j];
}
}
if(j>P.length){
return i-P.length;
}
return -1;
}
next数组是什么?匹配不上时子串指针该移动到指定的位置,我们跟着下面的流程走一遍就明白了:
为了效果更明显,我们设主串为AAABAAC,子串为AAC
第一轮:
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | c[7] | |
---|---|---|---|---|---|---|---|
主串 | A | A | A | B | A | A | C |
子串 | A | A | C |
当主串指针移动到c[3]时,发现子串和主串不匹配,主串指针不用回溯,将子串的指针移动到第二个位置,进行第二轮:
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | c[7] | |
---|---|---|---|---|---|---|---|
主串 | A | A | A | B | A | A | C |
子串 | A | A | C | ||||
子串指针 | 0 | 1 | 2 | 3 |
然后主串指针和子串指针继续向后匹配:
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | c[7] | |
---|---|---|---|---|---|---|---|
主串 | A | A | A | B | A | A | C |
子串 | A | A | C |
发现c[4]位置主串和子串不匹配,将子串指针移动到第二个位置:
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | c[7] | |
---|---|---|---|---|---|---|---|
主串 | A | A | A | B | A | A | C |
子串 | A | A | C | ||||
子串指针 | 1 | 2 | 3 |
发现还是不匹配,继续将指针移动到第一个位置:
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | c[7] | |
---|---|---|---|---|---|---|---|
主串 | A | A | A | B | A | A | C |
子串 | A | A | C | ||||
子串指针 | 1 | 2 | 3 |
发现不匹配,将子串指针移动到0位置:
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | c[7] | |
---|---|---|---|---|---|---|---|
主串 | A | A | A | B | A | A | C |
子串 | A | A | C | ||||
子串指针 | 0 | 1 | 2 | 3 |
然后,主串和子串指针同时向后匹配:
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | c[7] | |
---|---|---|---|---|---|---|---|
主串 | A | A | A | B | A | A | C |
子串 | A | A | C |
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | c[7] | |
---|---|---|---|---|---|---|---|
主串 | A | A | A | B | A | A | C |
子串 | A | A | C |
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | c[7] | |
---|---|---|---|---|---|---|---|
主串 | A | A | A | B | A | A | C |
子串 | A | A | C |
匹配成果,返回!看完之后是不是很懵?为什么知道子串该移动到哪?下面来看规则:
在第一轮中,c[3]位置匹配不上:
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | c[7] | |
---|---|---|---|---|---|---|---|
主串 | A | A | A | B | A | A | C |
子串 | A | A | C |
可以肯定的是c[3]前面的串一定匹配上了,现在就是让子串指针移动到某个位置使得亮色字符串中主串中的尾部和子串中的头部匹配上就可以了,一图胜千言 :
找主串中和子串中颜色相同的尾部和头部
名词解释:
- 主串中的尾部:不包含第一个字母的串,例如ABCD的尾部为 D、CD、BCD 总共三个。我们称{D、CD、BCD}为后缀集
- 子串中的头部:不包含最后一个字母的串,例如ABCD的头部为A、AB、ABC 总共三个。我们称{A、AB、ABC}为前缀集
求出主串和子串的前后缀:
主串后缀:A
子串前缀:A
所以子串指针移动到第二个位置:
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | c[7] | |
---|---|---|---|---|---|---|---|
主串 | A | A | A | B | A | A | C |
子串 | A | A | C | ||||
子串指针 | 1 | 2 | 3 |
指针继续向右匹配:
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | c[7] | |
---|---|---|---|---|---|---|---|
主串 | A | A | A | B | A | A | C |
子串 | A | A | C |
指针走到c[4]位置发现不匹配了,然后继续找主串的后缀和子串的头部相同的串:
主串后缀:A
子串前缀:A
所以子串的指针移动到第二个位置:
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | c[7] | |
---|---|---|---|---|---|---|---|
主串 | A | A | A | B | A | A | C |
子串 | A | A | C | ||||
子串指针 | 1 | 2 | 3 |
此时,发现c[4]不匹配
主串后缀:∅
子串前缀:∅
所以子串的指针移动到第一个位置:
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | c[7] | |
---|---|---|---|---|---|---|---|
主串 | A | A | A | B | A | A | C |
子串 | A | A | C | ||||
子串指针 | 0 | 1 | 2 | 3 |
发现还是不匹配,子串的指针移动到0位置:
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | c[7] | |
---|---|---|---|---|---|---|---|
主串 | A | A | A | B | A | A | C |
子串 | A | A | C | ||||
子串指针 | 0 | 1 | 2 | 3 |
然后主串的指针和子串的指针同时移动进行匹配:
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | c[7] | |
---|---|---|---|---|---|---|---|
主串 | A | A | A | B | A | A | C |
子串 | A | A | C |
规则到此就演示完了,所以你会发现这就是找前缀和后缀,然后取最长相同的前后缀,我们把前缀和后缀匹配出来的信息存到一个next数组里面,子串在哪个位置匹配失败就去next数组的哪个位置取它的指针的位置。
所以,子串AAC的next数组为:
索引 | next[1] | next[2] | next[3] |
---|---|---|---|
值 | 0 | 1 | 2 |
如何手动计算字符串的next数组?
用字符串AAABAAC为例,演示下方法:
- 第一个位置匹配不上:
c[0] | c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | c[7] | |
---|---|---|---|---|---|---|---|---|
主串 | A | A | A | B | A | A | C | |
子串 | C | C | C | |||||
子串指针 | 0 | 1 | 2 | 3 |
第一个位置匹配不上,说明应该和主串指针后面的值进行比较,只有把子串的指针移动到0位置,然后主串和子串的指针同时后移:
主串指针 | c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | c[7] |
---|---|---|---|---|---|---|---|
主串 | A | A | A | B | A | A | C |
子串 | C | C | C | ||||
子串指针 | 0 | 1 | 2 | 3 |
所以next[1]=0;
- 第二个位置匹配不上:
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | c[7] | |
---|---|---|---|---|---|---|---|
主串 | A | A | A | B | A | A | C |
子串 | A | C | C | ||||
子串指针 | 1 | 2 | 3 |
主串的前缀和子串的后缀都为空,所以next[2] = 1;
- 第三个位置匹配不上:
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | c[7] | |
---|---|---|---|---|---|---|---|
主串 | A | A | A | B | A | A | C |
子串 | A | A | C | ||||
子串指针 | 1 | 2 | 3 |
能匹配上的串为AA
主串的后缀:A
子串的前缀:A
所以next[3]= 2;
- 第4个位置匹配不上:
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | c[7] | |
---|---|---|---|---|---|---|---|
主串 | A | A | A | B | A | A | C |
子串 | A | A | A | Z | |||
子串指针 | 1 | 2 | 3 | 4 |
能匹配上的串为AAA
主串的后缀:A,AA
子串的前缀:A,AA
所以next[4]=3;
- 第五个位置匹配不上:
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | c[7] | |
---|---|---|---|---|---|---|---|
主串 | A | A | A | B | A | A | C |
子串 | A | A | A | B | Z | ||
子串指针 | 1 | 2 | 3 | 4 | 5 |
能匹配上的串为AAAB
主串的后缀:B,AB,AAB
子串的前缀:A,AA,AAA
所以next[5]=1;
- 第六个位置匹配不上:
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | c[7] | |
---|---|---|---|---|---|---|---|
主串 | A | A | A | B | A | A | C |
子串 | A | A | A | B | A | Z | |
子串指针 | 1 | 2 | 3 | 4 | 5 | 6 |
能匹配上的串为AAABA
主串的后缀:A,BA,ABA,AABA
子串的前缀:A,AA,AAA,AAAB
所以next[6]=2;
- 第七个位置匹配不上:
c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | c[7] | |
---|---|---|---|---|---|---|---|
主串 | A | A | A | B | A | A | C |
子串 | A | A | A | B | A | A | Z |
子串指针 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
能匹配上的串为AAABAA
主串的后缀:A,AA,BAA,ABAA,AABAA
子串的前缀:A,AA,AAA,AAAB,AAABA
所以next[7]=3;
所以字符串AAABAAC的next数组为:
索引 | next[1] | next[2] | next[3] | next[4] | next[5] | next[6] | next[7] |
---|---|---|---|---|---|---|---|
值 | 0 | 1 | 2 | 3 | 1 | 2 | 3 |
逻辑清晰了,再看看代码是不是就明白了呢!
小试牛刀
我们将主串S="AAABAAC"和子串P="AAC"和子串的next数组带入程序中试试呗:
// 用S代表主串,P代码待匹配的子串
int KMP(S, P)
{
int i=1, j=1;
while(i<=S.length && j<=P.length)
{
// j==0就说明主串中当前指针所指元素和子串中第一个元素不同,next[0]=0,指针+1后指向第一个位置
// S[i]==P[j]说明可以匹配,两者指针同时后移
if(j==0 || S[i]==P[j])
{
i++;
j++;
}
else
{
// 移动子串的指针到指定位置
j=next[j];
}
}
if(j>P.length){
return i-P.length;
}
return -1;
}
三、如何用程序计算next数组
上面我们是手算的next数组,那怎么用程序来计算next数组呢?
上代码:
void getNext(char c[],int next[]){
int i=1,j=0;
next[1]=0; // 第一个匹配不上就移动到子串的第0个位置
while(i<c.length-1){
if(j==0||c[j]==c[i]){ // j等于0 或者 主串和子串的当前指针位置的值相等时,两者指针同时后移
i++;
j++;
next[i]=j;
}else{// 子串和主串匹配不上时,此时子串指针在哪个位置,就去next数组里面取指针该移动到的目标位置
j=next[j];
}
}
}
四、next数组的优化
next数组是针对于匹配串而言的,也就是子串才有next数组
在上面我们手动算出字符串AAABAAC的next数组为:
next数组 | next[1] | next[2] | next[3] | next[4] | next[5] | next[6] | next[7] |
---|---|---|---|---|---|---|---|
值 | 0 | 1 | 2 | 3 | 1 | 2 | 3 |
在匹配过程中,匹配串指针移动到第二个位置时:
索引 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
匹配串 | A | A | A | B | A | A | C |
next数组值 | 0 | 1 | 2 | 3 | 1 | 2 | 3 |
发现不匹配,此时匹配串指针移动到next[2]也就是1位置,但是我们发现位置2为A匹配不上,位置1也为A肯定也匹配不上,所以指针要继续移动到next[1]也就是0位置,避免指针重复移动,我们直接把next[2]的值修改为0:
索引 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
匹配串 | A | A | A | B | A | A | C |
next数组值 | 0 | 0 | 2 | 3 | 1 | 2 | 3 |
匹配串指针移动到第三个位置时不匹配,指针就移动到next[3]也就是2位置,发现2位置与3位置匹配不上的字符相同,所以next[3]更新为next[2]的值:
索引 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
匹配串 | A | A | A | B | A | A | C |
next数组值 | 0 | 0 | 0 | 3 | 1 | 2 | 3 |
重复以上步骤,得到最终的next数组:
索引 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
匹配串 | A | A | A | B | A | A | C |
next数组值 | 0 | 0 | 0 | 3 | 0 | 0 | 3 |
我们将优化后的数组命名为:nextval 数组。