KMP 算法相对于 Brute-Force 算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。
暴力BF匹配
int BF(char S[], char T[])
{
int i = 0, j = 0;
while(S[i] != '\0' && T[j] != '\0'){
if(S[i] == T[j]){
i++; j++;
}
else {
i = i-j+1;//主串指针的回溯
j = 0;
}
}
if(T[j] == '\0') return (i-j); //主串中存在该模式返回下标号
else return -1; //主串中不存在该模式
}
提取加速匹配信息
KMP算法主要是通过消除主串指针的回溯来提高匹配的效率的,实际上是提取并运用了加速匹配的信息!
对于模式串 t 的每个元素 t j,都存在一个实数 k ,使得模式串 t 开头的 k 个字符(t0, t1,…,tk-1)依次与 tj 前面的 k 个字符(tj-k,tj-k+1,…,tj-1 这里第一个字符 tj-k 最多从 t1 开始,所以 k < j)相同。如果这样的 k 有多个,则取最大的一个。采用 next 数组表示,即 next[j] = MAX{k}。
void Getnext(int next[], String t)
{
int j = 0,k = -1;//公共前后缀的长度
next[0] = -1;
while(j < t.length-1)
{
if(k == -1 || t[j] == t[k])
{
j++; k++;
next[j] = k;
}
else k = next[k];//递归寻找更短的公共前后缀
}
}
next 求解过程
1. 当 j = 0 或 1 的情况
当 j 的值为 0 或 1 的时候,它们的 k 值都为 0,即 next[0] =0, next[1] =0。但是为了后面 k 值计算的方便,我们将 next[0] 的值设置成 -1。
2.当 t[j] == t[k] 的情况
当 t[j] == t[k] 时,必然有"t[0]…t[k-1]" == “t[j-k]…t[j-1]”,此时的 k 即是相同子串的长度,即next[j] = k。因为有"t[0]…t[k-1]" == “t[j-k]…t[j-1]” 且 t[j] == t[k],则有"t[0]…t[k]" == " t[j-k]…t[j]",这样也就得出了next[j+1] = k+1。
3. 当t[j] != t[k] 的情况
最长公共前后缀
字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;字符串的后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。
例如对于字符串 abacaba,其前缀有 a, ab, aba, abac, abacab,后缀有bacaba, acaba, caba, aba, ba, a。最长公共前后缀就是 aba。
字符串"P0…PkPk+1…Pj"中子串"P0…Pk"的最长公共前后缀长度用next数组表示为 next[k+1]。
递归寻找最长公共前后缀:在 Pk != Pj 时,k = next[k],用 Pnext[k] 去跟 Pj 继续匹配。
在 Pj 之前,有一段长度为 next[ j ] = k 的已匹配串,即"P0…Pk-1" = “Pj-k…Pj-1”,则子串"P0…Pk-1"为串"P0…Pj-1"的最长公共前后缀。
在 Pk 之前,有一段长度为 k’ = next[k] 的已匹配串,即"P0…Pk’-1" =“Pk-k’…Pk-1”(蓝色部分),则子串"P0…Pk’-1"为"P0…Pk-1"的最长公共前后缀。
由以上两步,则"P0…Pk’-1"=“Pj-k’…Pj-1”,如果 Pnext[k] 能和 Pj 匹配 ,那么寻找到"P0…Pj"的最长公共前后缀"P0…Pnext[k]";如果还是不匹配,下一步 Pnext[next[k]…] 去跟Pj继续匹配,直到找到长度更短公共前后缀。
KMP 算法实现
当 “Si-j…Si-1” == “T0 … Tj-1”,但 Si != Tj ,而"T0…Tk" == “Tj-k…Tj-1”,则next[j] = k。
由上 可知,“T0…Tk” == “Si-k…Si-1”。
将模式串T右移j-k个字符,继续匹配Si与Tk,而不是模式串T只右移1个字符,且避免目标串S的指针回溯从头和模式串T进行匹配。
int KMP(String s, String t)
{
int next[MaxSize], i = 0, j = 0;
Getnext(t, next);//得到next[]数组
while(i < s.length && j < t.length)
{
if(j == -1 || s[i] == t[j])
{
i++;
j++;
}
else j = next[j]; //j回退,更短公共前后缀
}
if(j >= t.length)
return (i-t.length); //匹配成功,返回子串的位置
else
return (-1); //没找到
}
改进后 next 求解方法
目标串S=“ABACDA”;
模式串T=“ABAB” 且 next数组应该是[ -1,0,0,1 ](可由最长公共前后缀方法求);
当 Si != Tj 时,且next[j] = 1,将 j 移动到第1个元素,进行匹配。
这一步是完全没有意义的,因为后面的B已经不匹配了,那前面的B也一定是不匹配的,同样的情况其实还发生在第 2 个元素A上。显然,发生问题的原因在于t[j] == t[next[j]]。
void Getnext(int next[], String t)
{
int j = 0, k = -1;
next[0] = -1;
while(j < t.length-1)
{
if(k == -1 || t[j] == t[k])
{
j++; k++;
if(t[j] == t[k])//当两个字符相同时,就跳过
next[j] = next[k];
else
next[j] = k;
}
else k = next[k];
}
}