KMP算法详解
文章目录
1. KMP算法简介
KMP算法是一种用于字符串匹配的算法,从一串字符串中查询,是否包含某个子串。比如
const char *a = "abcdefabcdee"; //主串
const char* b = "def"; //模式串
b就是属于a的子串,a的起始下标为3。我们把长的串那个叫做主串,用于匹配的串叫做模式串
2. KMP算法的前身—暴力破解法
起初人们匹配字符串使用的是所谓的暴力破解法,我们举个例子,从主串S="goodgoogle"
,寻找子串T="google"
,具体步骤为:
其实就是不断的将模式串与主串进行匹配,如果匹配不成功,主串回溯到原来位置的下一个位置,然后模式串回溯到最开头。具体代码为:
#include<iostream>
#include<cstring>
using namespace std;
int ViolentMatch(const char* S, const char* T)
{
int Slen = strlen(S); //主串
int Tlen = strlen(T); //模式串
int i = 0;
int j = 0;
while (i < Slen &&j < Tlen)
{
if (S[i] == T[j])
{
i++; //字符串如果匹配,就继续向前搜索
j++;
}
else
{
i = i - j + 1; //如果不匹配,主串回退搜索的长度,并且往后偏移一个位置
j = 0; //模式串从头继续搜索
}
}
if (j == Tlen)
{
return i - j; //返回匹配位置
}
else
{
return -1;
}
}
int main()
{
const char* a = "ABCERDS";
const char* b = "RD";
int n = ViolentMatch(a, b);
cout << n << endl;
return 0;
}
2.KMP算法的实现
2.1 KMP算法的思路
一般来说,普通的暴力破解法能够满足我们的需求,但是对于一些计算量很大的搜索,就显得运算效率过低,比如在一本书中搜索一个专有名词。我们还是通过一个例子引入,说明KMP算法的优越之处。
主串S=“abcdefgab”,模式串T = “abcdex”。如果用暴力搜索,过程为:
其实这里面步骤2-5都是没有意义的。因为本身模式串的前5个字符完全不同,又和主串完全匹配,所以主串的第2-5个位置不可能有与模式串第1个位置相同的元素。所以,直接进入第6步骤即可。这就是KMP算法的高明之处。
如果模式串中出现了重复的部分,比如主串S=“abcabcabc",模式串T="abcabx”,匹配过程为
与前面相同,2和3步骤是多余的。而由于模式串中有相同的子串ab,在4和5中,主串中的ab已经在第1步骤中与模式串第二个ab成功匹配过了,所以,当模式串第一个ab过来的时候,不需要二次匹配。因此步骤4和5也是多余的。
因此我们知道了,KMP算法的核心就是:
- 寻找不匹配的结点位置
- 根据模式串的特征,往后进行平移到一个合适的位置继续与主串进行比对,使得主串不需要进行指针回溯。
2.2 对模式串特征的描述
在KMP算法中,我们需要一个很重要的东西,就是对模式串的特征描述。因为我们需要知道,当模式串与主串不匹配的时候,应该怎么把模式串移动到什么位置继续匹配
比如模式串 T=“ABABAAA”;
(1)如果有主串 S = “BBBBBBBBB”
主串 | B | B | B | B | B | B | B | B | B | |
---|---|---|---|---|---|---|---|---|---|---|
模式串 | A | B | A | B | A | A | A |
当主串与模式串的第一个位置不匹配的时候,模式串往后移动一位,跳过主串第一位置,继续匹配。
主串 | B | B | B | B | B | B | B | B | B | |
---|---|---|---|---|---|---|---|---|---|---|
模式串 | A | B | A | B | A | A | A |
(2)如果有主串 S = “ACBBBBBBB”
主串 | A | C | B | B | B | B | B | B | B | |
---|---|---|---|---|---|---|---|---|---|---|
模式串 | A | B | A | B | A | A | A |
当第二个位置不匹配的时候,模式串往后移动一位,下标为0的元素继续与第二位匹配.
主串 | A | C | B | B | B | B | B | B | B | |
---|---|---|---|---|---|---|---|---|---|---|
模式串 | A | B | A | B | A | A | A |
(3)如果有主串 S = “ABBBBBBBB”
主串 | A | B | B | B | B | B | B | B | B | |
---|---|---|---|---|---|---|---|---|---|---|
模式串 | A | B | A | B | A | A | A |
当第三个位置不匹配的时候,模式串往后移动两位,下标为0的元素继续与第三位匹配.
主串 | A | B | B | B | B | B | B | B | B | |
---|---|---|---|---|---|---|---|---|---|---|
模式串 | A | B | A | B | A | A | A |
(4)如果有主串 S = “ABACBBBBBB”
主串 | A | B | A | C | B | B | B | B | B | B | |
---|---|---|---|---|---|---|---|---|---|---|---|
模式串 | A | B | A | B | A | A | A |
当第四个位置不匹配的时候,模式串往后移动,下标为1的元素继续与第四位匹配.
主串 | A | B | A | C | B | B | B | B | B | B | |
---|---|---|---|---|---|---|---|---|---|---|---|
模式串 | A | B | A | B | A | A | A |
(5)如果有主串 S=“ABABCCCCCCCCC”
主串 | A | B | A | B | C | C | C | C | C | C | C | C | C | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
模式串 | A | B | A | B | A | A | A | A |
当第五个位置不匹配的时候,模式串往后移动,下标为2的元素继续与第五位匹配.
…
其余省略,这种当模式串第n个位置不匹配的时候,下标为i的元素继续与主串原位置匹配的描述,就是对模式串特征的描述
2.3 next数组含义
2.2个那种对模式串的特征的描述,实际上就是在找,当模式串第i个位置不匹配的时候,前i-1个元素最长公共前后缀的长度是多少。求这个长度获得的结果,就是next数组,表征某个位置不匹配的时候,模式串应该移动到哪个下标位置继续与主串匹配。比如上面的模式串ABABAAA,这个模式串的next数组就是
A | B | A | B | A | A | A |
---|---|---|---|---|---|---|
-1 | 0 | 0 | 1 | 2 | 1 | 1 |
举例说明,比如下标为4的元素对应的最长公共前后缀长度是2。这是因为,下标为0-3的元素是ABAB,这个子串的前缀可以是A,AB,ABA;这个子串的后缀可以是B,AB,BAB,可以看出,最长公共前后缀为AB,长度为2,就填入下标4的位置。也就是标识,当下标为4的元素与主串不匹配的时候,应该从下标为2的元素继续开始与主串进行匹配。
这里需要对-1进行解释,因为一旦第一个位置不匹配,主串的相应位置就会被跳过,由下一个元素继续与模式串的下标为0的元素进行匹配,为了标记这种不同的跳过操作,所以使用-1作为标记。
2.4 next数组的求法
求next数组有比较巧妙的方法。我们并不直接求next[j]的最长公共前后缀,而是利用next[j-1]来求next[j]。
比如T=ABAB最长公共前后缀是AB,假设前缀最后一个下标k,也就是T[k]=B;后缀最后一个下标是i,即T[i]=B,最长公共前后缀为2。
(1)如果,T[k+1]=T[i+1],就比如T = ABABA,如果前缀的下一个元素和后缀的下一个元素继续匹配,则这个串的最长公共前后缀数会比原来+1。
即T[k+1]=T[i+1]的时候,next[i+2]=next[i+1]+1
A | B | A | B | A | E |
---|---|---|---|---|---|
2 | 3 |
也就是next[5]=next[4]+1
(2)如果T[k+1]!=T[i+1]会怎么样呢?比如下面这个
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
T | A | B | C | A | B | D | A | B | C | A | B | E | F |
next | -1 | 0 | 0 | 0 | 1 | 2 | 0 | 1 | 2 | 3 | 4 | 5 |
对于next[11]来说前缀ABCAB和后缀ABCAB匹配,也就是T[k]=T[4],T[j]=T[10],而T[k+1]=D,T[j+1]=E,二者并不相等。这个时候如果要求next[j+1],我们应该查找next[K+1],如果不为0,说明0-k部分还有更小一级的对称的部分可以继续考虑。
n
e
x
t
[
j
+
1
]
=
n
e
x
t
[
k
+
1
]
next[j+1]=next[k+1]
next[j+1]=next[k+1]
在这里就是继续比对
T [ n e x t [ k + 1 ] ] = = T [ i + 1 ] T[next[k+1]]==T[i+1] T[next[k+1]]==T[i+1]
从表中我们看出,虽然0-5的元素ABCABD与6-11的元素ABCABE不匹配,但是0-4的元素有一个公共前后缀AB,与9-10元素完全一样,如果T[2]=T[11]的话,仍然满足T[i+1]=T[k]+1的关系。但是T[2]并不等于T[11],于是继续从0-1中查找是否还有公共前后缀有比较的价值。
2.5 实现next表的创建
void get_next(const char* t,int * next) //获取回溯的表
{
int len = strlen(t);
int i = 0;//尾缀
int j = -1; //前缀
next[0] = -1;
while (i < len)
{
if (j == -1 || t[i] == t[j])
{
j++;
i++;
next[i] = j;
}
else
{
j = next[j]; //如果没匹配上,寻找有没有对称的子列,顺着上一个对称的位置继续匹配
//比如ABCABDABCABE ,前面ABCAB和后面ABCAB是完全匹配的,但是ABCABD和ABCABE不匹配,
//我们就要把指向D位置的j指针回溯,回溯到哪里呢?我们发现ABCABD前面的ABCAB是有对称元素的
//那么ABD与后面不匹配,缩小范围,前面的ABC有没有可能与后面的继续匹配呢?如果匹配了,就顺着
//这个地方的next值继续+1,不匹配就再往前回溯
}
}
}
2.6 KMP算法的实现
有了next数组之后,KMP算法就容易了。我们向后依次比对,如果在模式串第i个位置不匹配,查询模式串的next[i]作为回溯值,继续与主串这个位置匹配。如果回溯值为-1,说明主串的元素该向后移动了。
#include<iostream>
#include<cstring>
using namespace std;
string t="ababaaaba";
//j-2 j-1 j i-2 i-1 i
//获取next表思路是
//(1)如果前面字符都配上了比如 A B C A B C ,如果j+1和i+1还能继续配对,那么
//next[i+1] = next[i]+1 匹配数+1
//(2) 如果next[i+1]和next[j+1]没配上,我们就找next[j+1]的next表值,如果不是0,说明0-j部分,还是有对称
//的元素,我们就把j回溯到前一个对称值的下一个位置,再更小范围内搜索看看有没有匹配项
void get_next(const char* t,int * next) //获取回溯的表
{
int len = strlen(t);
int i = 0;
int j = -1; //尾缀
next[0] = -1; //前缀
while (i < len)
{
if (j == -1 || t[i] == t[j])
{
j++;
i++;
next[i] = j;
}
else
{
j = next[j]; //如果没匹配上,寻找有没有对称的子列,顺着上一个对称的位置继续匹配
//比如ABCABDABCABE ,前面ABCAB和后面ABCAB是完全匹配的,但是ABCABD和ABCABE不匹配,
//我们就要把指向D位置的j指针回溯,回溯到哪里呢?我们发现ABCABD前面的ABCAB是有对称元素的
//那么ABD与后面不匹配,缩小范围,前面的ABC有没有可能与后面的继续匹配呢?如果匹配了,就顺着
//这个地方的next值继续+1,不匹配就再往前回溯
}
}
}
int Index_KMP(const char*S,const char*T )
{
int next[100];
get_next(T, next);//获取模式串用于标识回溯位置的表
int i = 0;
int j = 0;
int Slen = strlen(S);//主串
int Tlen = strlen(T); //模式串
while (i < Slen && j < Tlen)
{
if (j==-1||S[i] == T[j])
{
i++;
j++;
}
else
{
j = next[j];
}
}
if (j == Tlen)//匹配成功
{
return i - j;
}
else
{
return -1;
}
}
int main()
{
const char* b = "AAAAAIIIISLSLSLABCABDABCABEPPPPSS";
const char* a= "ABCABDABCABE";
int n = Index_KMP(b, a);
cout << n << endl;
return 0;
}
2.7 KMP算法总结
KMP算法比暴力破解法,优越的地方在于,保留了每次匹配的成功之处,下次可以从失败处继续,也就是从哪里跌倒就在哪里爬起来。
3.KMP算法的优化
3.1 KMP算法的问题
遗憾的是,KMP并非是完美无缺的,因为它只总结了每次匹配的成功,却没有在匹配失败的时候进行总结。比如
主串 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | |
---|---|---|---|---|---|---|---|---|---|
模式串 | 0 | 0 | 0 | 0 |
模式串的next数组为
0 | 0 | 0 | 0 |
---|---|---|---|
-1 | 0 | 1 | 2 |
在模式串的第4个位置匹配失败后,会回溯到下标为2的位置继续与主串匹配,但是模式串下标0,1,2元素都与3一致,3都不一样的,0-2的元素没必要继续匹配了。而依照next表,模式串会继续与主串匹配
主串 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | |
---|---|---|---|---|---|---|---|---|---|
模式串 | (1) | 0 | 0 | 0 | 0 | ||||
(2) | 0 | 0 | 0 | 0 | |||||
(3) | 0 | 0 | 0 | 0 | |||||
(4) | 0 | 0 | 0 | 0 |
其实上面(1)到(3)部分都是多余的。为了改进KMP算法,我们在求next数组的时候,进行了新的改进
3.2 next数组的改进
其实求这个next数组的时候,我们就需要记住一点,在某个失败的位置,如果要往前回溯,回溯位置的值与现在位置的值如果相同,应该放弃这个位置,继续向前回溯。
void get_next(const char* t, int* next) //获取回溯的表
{
int len = strlen(t);
int i = 0;
int j = -1; //尾缀
next[0] = -1; //前缀
while (i < len)
{
if (j == -1 || t[i] == t[j])
{
j++;
i++;
//与普通KMP差异的地方
if (t[i] != t[j])
{
next[i] = j;
}
else
{
next[i] = next[j];
//这个主要是考虑匹配失败的时候,如果匹配失败了,必然往回回溯,但是如果回溯的下一个
//地方还是这个元素,必然没有意义,还要往前回溯,那就把前一个元素的回溯值直接交给后面就可以了
//比如 S = abacababc
// T = abab
//匹配第四个元素不同,必然往前回溯,变成
// S = abacababc
// T = abab
//对着的还是b,不可能匹配成功的。
//所以,如果有匹配的子串,而且其下一个元素也相同,那就意味着,如果下一个元素匹配失败的时候
//往前回溯,对应元素还是这个值。
}
//差异结束
}
else
{
j = next[j];
}
}
}
3.3 改进的KMP算法实现
#include<iostream>
#include<cstring>
#include<vector>
using namespace std;
string t = "ababaaaba";
void get_next(const char* t, int* next) //获取回溯的表
{
int len = strlen(t);
int i = 0;
int j = -1; //尾缀
next[0] = -1; //前缀
while (i < len)
{
if (j == -1 || t[i] == t[j])
{
j++;
i++;
//与普通KMP差异的地方
if (t[i] != t[j])
{
next[i] = j;
}
else
{
next[i] = next[j];
//这个主要是考虑匹配失败的时候,如果匹配失败了,必然往回回溯,但是如果回溯的下一个
//地方还是这个元素,必然没有意义,还要往前回溯,那就把前一个元素的回溯值直接交给后面就可以了
//比如 S = abacababc
// T = abab
//匹配第四个元素不同,必然往前回溯,变成
// S = abacababc
// T = abab
//对着的还是b,不可能匹配成功的。
//所以,如果有匹配的子串,而且其下一个元素也相同,那就意味着,如果下一个元素匹配失败的时候
//往前回溯,对应元素还是这个值。
}
//差异结束
}
else
{
j = next[j];
}
}
}
int Index_KMP(const char* S, const char* T)
{
int next[100];
get_next(T, next);//获取模式串用于标识回溯位置的表
int i = 0;
int j = 0;
int Slen = strlen(S);//主串
int Tlen = strlen(T); //模式串
while (i < Slen && j < Tlen)
{
if (j == -1 || S[i] == T[j])
{
i++;
j++;
}
else
{
j = next[j];
}
}
if (j == Tlen)//匹配成功
{
return i - j;
}
else
{
return -1;
}
}
int main()
{
const char* b = "AAAAAIIIISLSLSLABCABDABCABEPPPPSS";
const char* a = "ABCABDABCABE";
int n = Index_KMP(b, a);
cout << n << endl;
return 0;
}
4. 参考资料
【1】大话数据结构
【3】浙大-数据结构
【4】 清华-数据结构