KMP算法
KMP算法简介:
主要针对字符串的匹配问题,给出字符串a和b,判断b是否为a的子串。时间复杂度为O(m+n) 由Knuth、Morris 和 Pratt 三位科学家共同发现。
next数组简介:
一个字符串s(下标从0开始)则以i号位作为结尾的子串即s[0…i]。对该子串来说,长度为k+1的前缀和后缀分别为s[0…k]和s[i-k…i]。定义next数组,next[i]表示使子串s[0…i]的前缀s[0…k]等于后缀s[i-k…i]的最大的k,若找不到相等的前后缀,则令next[i] = -1。即next[i]是所求最长相等前后缀最后一位的下标。
next数组的求解举栗🌰:
next数组求解过程:
①i=0:子串s[0…i]为"a",由于找不到相等的前后缀,因此令next[0] = -1。
②i=1:子串s[0…i]为"ab",由于找不到相等的前后缀,因此令next[1] = -1。
③i=2: 子串s[0… i]为"aba",能使前后缀相等的最大的k=0,此时后缀i[i-k…i]为"a",前缀s[0…k]也为"a";而当k=1时,后缀[i-k…i]为"ba",前缀s[0… k]为"ab",不相等,因此next[2] = 0。
④i=3: 子串s[0… i]为"abab",能使前后缀相等的最大的k=1,此时后缀s[i-k…i]为"ab",前缀s[0… k]也为"ab";而当k = 2时后缀s[i- k… i]为"bab",前缀s[0… k]为"aba",不相等,因此next[3] = 1。
⑤i=4:子串s[0… ij]为"ababa",能使前后缀相等的最大的k=2,此时后缀s[i-k…i]为"aba",前缀s[0… k]也为"aba";而当k= 3时后缀s[i-k… i]为"baba",前缀s[0… k]为"abab",不相等,因此next[4] = 2。
⑥i=5:子串s[0… i]为"ababaa",能使前后缀相等的最大的k=0,此时后缀s[i-k…i]为"a",前缀s[0…k]也为"a";而当k = 1时后缀s[i-k…i]为"aa",前缀s[0…k]为"ab",不相等,因此next[5] = 0。
⑦i=6: 子串s[0… ij]为"ababaab",能使前后缀相等的最大的k=1,此时后缀s[i-k…i]为"ab",前缀s[0…k]也为"ab";而当k = 2时后缀s[i-k… i]为"aab",前缀s[0… k]为"aba",不相等,因此next[6] = 1。
求解next数组的一般思路:
①初始化next数组,令j = next[0] = -1。
②i在1~len-1遍历,对每一个i执行③④过程以求解next数组。
③不断令j = next[j],直到j回退为-1,或s[i] = s[j+1]成立。
④若s[i] = s[j+1],则next[i] = j+1,否则next[i] = j。
求解next数组的代码实现:
void getnext(char s[],int len){
int j = -1;
next[0] = -1; //初始化j = next[0] = -1
for(int i = 1;i < len; i++){
while(j != -1 && s[i] != s[j+1]){
j = next[j]; //不断令j = next[j],直到j回退为-1,或s[i] = s[j+1]成立
}
if(s[i] == s[j+1]){ //若s[i] = s[j+1]
j++; //则next[i] = j+1,令j指向此位置
}
next[i] = j; //令next[i] = j
}
}
KMP算法举栗🌰:
以a=“abababaabc”、b="ababaab"为例,令i指向字符串a的当前想比较的位置,令j指向字符串b中当前已被匹配的最后位,只要满足a[i] = b[j+ 1]成立,则说明b[j + 1]被成功匹配,此时让i、j 加1继续进行比较,直到 j 达到m - 1时说明b是a的子串(m为字符串b的长度)。i指向a[4]、 j指向b[3], 表明b[0… 3]已经全部成功匹配,此时发现a[i] = b[j + 1]成立,表明b[4]成功匹配,则继续令i、j加1。
接着继续进行配,如下图所示。此时i指向a[5]、j指向b[4],表明b[0…4]已经全部匹配成功。接着继续判断a[i] = b[j + 1]是否成立:若成立,则b[0… 5]被成功匹配,令i、 j加1以继续匹配下一位,然而ButHowever!!!此处a[5] != b[4+ 1],匹配失败。啊这啊这啊这…,难道就此放弃之前b[0… 4]的成功匹配成果、让j回退到-1开始重新匹配???NO!!!
那应该咋办呢?为了不让j直接回退到-1,需要寻求回退到一个离当前的j最近的j’,使a[i] = b[j’ + 1]成立,并且b[0… j’]是与a的相对应位置处于匹配状态,即b[0… j‘]是b[0… j]的后缀。 容易令人想到这就是之前求next数组时碰到的类似问题,b[0… j’]就是b[0… j]的最长相等前后缀。 只需要不断令j= next[j],直到j回退到-1或a[i] = b[j + 1]成立,然后继续匹配即可。next数组的含义就是当j+ 1位失配时,j应该回退到的位置。 当a[5]与b[5]匹配失败时,令j = next[4] = 2,然后惊喜地发现a[i] = b[j + 1]成立,因此继续匹配,直到j = 6也匹配成功,即b是a的子串!!!
KMP算法的一般思路:
①初始化j = -1,表明字符串b当前已被匹配的最后位。
②让i遍历字符串a,对每一个i执行③④过程以匹配a[i]和b[j+1]。
③不断令j = next[j],直到j回退为-1,或a[i] = b[j+1]成立。
④若a[i] = b[j+1],则 j++,若 j达到 m-1,则说明b是a的子串,返回true。
KMP算法的代码实现(判断b是否为a的子串):
bool KMP(char a[],char b[]){
int n = strlen(a);
int m = strlen(b); //字符串长度
getnext(b,m); //求解字符串b的next数组
int j = -1; //初始化j为-1,表示当前没有任意一位被匹配
for(int i = 0;i < n; i++){
while(j != -1 && a[i] != b[j+1]){
j = next[j]; //不断回退,直到j回到-1或a[i]=b[j+1]
}
if(a[i] == b[j+1]){
j++; //a[i]与b[j+1]匹配成功,则j++
}
if(j == m-1){ //字符串b是a的子串
return true;
}
}
return false;
}
通过比较代码发现:KMP算法的代码实现与求解next数组相似的很,于是乎发现next数组的求解过程即是字符串b的自我匹配过程!!!
接着可以进一步考虑如何统计字符串b在字符串a中出现的次数。例如对字符串a ="abababab"来说,字符串b = "abab"出现了三次,而"ababa"出现了两次。当 j = m- 1时表示字符串b的一次成功完全匹配,此时可以使记录成功匹配次数的变量加1,而之后应该从字符串b的哪个位置开始进行下一次匹配是个问题。下图所示是一次匹配成功的时刻,由于字符串b在字符串a中的多次出现可能是重叠出现的,因此不能直接让i加1继续比较,而应让j回退。此时又是next[j]的高光时刻!!!因为此时next[j]代表整个字符串b的最长相等前后缀,从此位置开始可以让j最大,也就是让已经成功匹配的部分最长,这样就能保证既不漏解,又使下一次的匹配省去无意义的比较。
KMP算法的代码实现(统计字符串b在a中出现的次数):
int KMP(char a[],char b[]){
int n = strlen(a);
int m = strlen(b); //字符串长度
getnext(b,m); //计算b的next数组
int count = 0; //成功匹配次数
int j = -1; //初始化
for(int i = 0;i < n; i++){
while(j != -1 && a[i] != b[j+1]){
j = next[j]; //不断回退,直到j回到-1或a[i]=b[j+1]
}
if(a[i] == b[j+1]){
j++; //a[i]与b[j+1]匹配成功,则j++
}
if(j == m-1){ //字符串b是a的子串
count++;
j = next[j]; //使j回退到next[j]继续匹配
}
}
return count;//返回成功匹配次数
}