1.什么是最大前后缀相等串
字符串abcab,他的所有前缀串是a,ab,abc,abca他的所有后缀串是bcab,cab,ab,b。他们的最长相等的串是ab。
下图是查找字符串abcaba的过程,当最后一个字符c不等于a时需要把模式串右移,暴力做法是每次不相等时移动一位,然后比较每个字符是否相等直到完全匹配为止。看下图可知第1步到第4步其实就是查询abcab的的最大相等的前后缀串的长度。如果我们事先已经求出了这个值,那么第2,3步直接可以跳过,并且直接从c字符往后继续比较就行。这就是kmp算法的核心。
ab是表格第一行字符串abcab的后缀串
步骤 | a | b | c | a | b | c | a | b | a | b | a |
1 | a | b | c | a | b | a | |||||
2 | a | b | c | a | b | a | |||||
3 | a | b | c | a | b | a | |||||
4 | a | b | c | a | b | a |
ab是表格第四行字符串abcab的前缀串
2.如何求最大前后缀相等串的长度
求ABBACABBAB的所有子串的最大前后缀相等的串的长度。
索引i | 最大前后缀相等的串长度len[i]的值 | next[i] | nextval | 子串字符串pstr |
0 | 0 | -1 | -1 | A |
1 | 0 | 0 | 0 | AB |
2 | 0 | 0 | 0 | ABB |
3 | 1 | 0 | -1 | ABBA |
4 | 0 | 1 | 1 | ABBAC |
5 | 1 | 0 | -1 | ABBACA |
6 | 2 | 1 | 0 | ABBACAB |
7 | 3 | 2 | 0 | ABBACABB |
j-1 | 4 | 3 | -1 | ABBACABBA |
j | ? len[j] | ? next[j] | ? nextval[j] | ABBACABBAB |
求子串pstr[j]的最大前后缀串长度len[j]的值。
通过表观察很显然len[j]的值依赖于前一个子串pstr[j-1]的解,如果字符p[j] = p[len[j-1]]相等,通俗的说就是前面最大前后缀串的基础上再加长1个字符也相等。那么len[j]=len[j-1] + 1。如果不相等就需要向前回溯。下面详细解释回溯。
例如本题,当j=9时,字符C不等于B,求ABBACABBAB的最大前后缀串的长度。
第一步,在红色ABBA字符串中找到一个最长前缀串str1,在蓝色字符串ABBA中找到一个最长后缀串str2,使得str1= str2;
第二步,找到最长str1=str2后,本例子是红色串"A", ABBA,再比较p[j]和p[strlen(str1)]这2字符,如果相等那么ABBACABBAB 的最大前后缀串的长度就是strlen(str1) + 1。如果不相等再次同样的逻辑回溯,直到回溯到第一个字符。本例子中ABBACABBAB,p[strlen(str1)] = B, p[j] = B,所以n[j] = 2。
重点:因为红色串是ABBA是pstr[j-1]的最大前缀串,蓝色串ABBA是pstr[j-1]的最大后缀串,所以ABBA=ABBA,所以第一步的问题就是找pstr[ n[j-1] - 1](长度1开始,索引0开始,长度对应到索引需要-1) 即pstr[3]的最大前后缀串。即是之前已经求出的n[3]值。这样取上一个结果的步骤就是回溯。
步骤 | a | b | c | a | b | c | a | b | a | b | a |
1 | a | b | c | a | b | a | |||||
2 | a | b | c | a | b | a | |||||
3 | a | b | c | a | b | a | |||||
4 | a | b | c | a | b | a |
从上面表格看,当字符c和a不等时,其实是找abcab的最大前后缀相等的串,是取用的不等字符往前偏移一位的len值。如果从哪个字符不等就取哪个位置的len值,那只需在len表的最前面插入一位就行,通常插入-1。这就是常说中的next表。 kmp的核心是len表,只需len表就能进行查找,为了编码方便引入了next表。为了进一步提高查询效率引入了nextval表。
index | 0 | 1 | 2 | 3 |
a | a | a | c | |
len | 0 | 1 | 2 | 0 |
next | -1 | 0 | 1 | 2 |
nextval | -1 | -1 | -1 | 2 |
步骤 | a | a | a | b | a | a | a | c |
1 | a | a | a | c | ||||
2 | a | a | a | c | ||||
3可优化 | a | a | a | c | ||||
4可优化 | a | a | a | c | ||||
5 | a | a | a | c |
上表是按照next表进行比较的流程,很明显第3,4步骤可以省略掉,因为第2步已经比较了b和a,后面步骤3,4的二次比较必定不相等。所以可以在next的表基础上再进行优化得道nextval表。
当p[j] != p[next[j]] 时nextval[j] = next[j],当p[j] == p[next[j]]时往前回溯。
下面代码展示了使用暴力循环,len表,next表,nextval表查询匹配字符串的方法。
void CreateTables(const char* pattern, int nlen[], int next[], int nextval[])
{
//当p[j] != src[i] 时,实际找的是pstr[j-1]的最大前后缀长度
//在nlen前插入-1,就是next数组
// ABABABD
//nlen 0012340
//next -1001234
int j = 0;
next[0] = -1;
nlen[j] = 0;//一个字符的串,最大前后缀串长度必定是0.
//循环求pattern所有子串的最大前后缀串长度
for(int j = 1; j < strlen(pattern); j++)
{
//前个解的基础上再加一个字符也相等,显然新解就是前面的解+1.
if(pattern[j] == pattern[ nlen[j-1] ])
{
nlen[j] = nlen[j-1] + 1;
}
else
{
int pre = nlen[j-1] - 1; // 回溯上一个解。 即回溯到字符串长度为n[j-1],索引位置为n[j-1]-1的解
while (pre >=0)
{
if(pattern[j] == pattern[ nlen[pre] ])
{
nlen[j] = nlen[pre] + 1;
break;
}
else
{
pre = nlen[pre] - 1; // 不相等继续回溯。
}
}
if(pre == -1)
nlen[j] = 0;
}
next[j] = nlen[j - 1];
}
// 根据nlen算偏移一位的nextval'
// for(int j = 1; j < strlen(pattern); ++j)
// {
// int k = nlen[j-1];
// while(k > 0)
// {
// if(pattern[j] != pattern[ k ])
// {
// nval[j-1] = k;
// break;
// }
// else
// {
// k = nlen[k -1];
// }
// }
// if(k == 0)
// nval[j-1] = 0;
// }
//根据next算nextval
for(int i = 0 ; i < strlen(pattern); ++i)
{
int k = next[i];
if(k>=0)
{
if(pattern[i] != pattern[k])
nextval[i] = k;
else
nextval[i] = nextval[k];
}
else
nextval[i] = k;
}
}
int FindStr(const char* src, const char* pattern)
{
if(strlen(src) < strlen(pattern))
return -1;
int *pnlen = new int[strlen(pattern)];
int *pnext = new int[strlen(pattern)];
int *pnextval = new int[strlen(pattern)];
CreateTables(pattern, pnlen, pnext, pnextval);
for(int i = 0 ;i < strlen(pattern); ++i)
{
printf("nlen:%d, next:%d, \tnextval:%d, \t%.*s\n", pnlen[i], pnext[i], pnextval[i], i+1 , pattern);
}
int index0 = -1;
//暴力循环 查找出匹配的第一个位置索引。
for(int start=0; start<strlen(src); ++start)
{
int i = start;
if(strlen(src) - i >= strlen(pattern))
{
bool bfind = true;
for(int j=0; j<strlen(pattern); ++j, ++i)
{
if(src[i] != pattern[j])
{
bfind = false;
break;
}
}
if(bfind)
{
index0 = start;
break;
}
}
}
int index1 = -1;
//根据nlen 查找出匹配的第一个位置索引。
{
for(int i = 0, j = 0; j < strlen(pattern) && i <strlen(src); )
{
if(src[i] != pattern[j])
{
if(j>=1)
j = pnlen[j-1];
else
i++;
}
else
{
if(j == strlen(pattern) - 1)
{
index1 = i - (int)strlen(pattern) + 1;
break;
}
else
{
j++;
i++;
}
}
}
}
int index2 = -1;
//根据next 查找出匹配的第一个位置索引。
{
for(int i = 0, j = 0 ; i < strlen(src);)
{
if(src[i] != pattern[j])
{
j = pnext[j];
if(j<0)
{
i++;
j = 0;
}
}
else
{
if(j == strlen(pattern) - 1)
{
index2 = i - (int)strlen(pattern) + 1;
break;
}
else
{
i++;
j++;
}
}
}
}
int index3 = -1;
//根据nextval 查找出匹配的第一个位置索引。
{
for(int i = 0, j = 0 ; i < strlen(src);)
{
if(src[i] != pattern[j])
{
j = pnextval[j];
if(j<0)
{
i++;
j = 0;
}
}
else
{
if(j == strlen(pattern) - 1)
{
index3 = i - (int)strlen(pattern) + 1;
break;
}
else
{
i++;
j++;
}
}
}
}
assert(index0 == index1);
assert(index1 == index2);
assert(index2 == index3);
return index1;
}
int main()
{
int i = FindStr("BABABABDAA", "ABABDA");
cout << "index:" << i << endl;
return 0;
}
运行结果
nlen:0, next:-1, nextval:-1, A
nlen:0, next:0, nextval:0, AB
nlen:1, next:0, nextval:-1, ABA
nlen:2, next:1, nextval:0, ABAB
nlen:0, next:2, nextval:2, ABABD
nlen:1, next:0, nextval:-1, ABABDA
index:3