注:这篇博客不是纯讲解类博客,只是个人学习记录。
智商下线越来越严重了,一个算法快看了一天,这还没算上课堂上看的时间,我太复杂了。
KMP算法是用来解决字符串的匹配问题的。
假定现在有两个字符串,一个文本串text,一个模式串patten。现在要求我们判断模式串pattern是否是text的字串,该怎么做呢?
pattern要想是text的子串,必须在text中找到一段与pattern完全相同的部分字符,于是很容易想到接下来的工作大致会涉及到以下两步:
- 遍历。text与pattern都需要遍历。
- 比较。不比较怎么可能知道字符间是否相等?
比较很简单,if语句加 == 运算符就可以,至于遍历,因为涉及到两条字符串我们需要引入两个哨兵变量i,j,初始时i,j分别指向pattern与text的头部,之后不断令i++,j++就行了。
最容易想到的并且最容易理解的是算法执行效率比较差的暴力算法:
bool StrMatch(chat *text,char *pattern)
{
int i = 0,j = 0;
int n = strlen(text);
int m = strlen(pattern);
while(i < n && j < m)
{
if(text[i] == pattern[j])
{
i++;
j++;
}
else
{
i = i - j + 1;
j = 0;
}
}
if(j == (m - 1))
return true;
return false;
}
在暴力匹配算法中,一旦出现匹配失败,i的值会回到最初开始匹配的位置的后一位,然后重新开始匹配。我们把这种操作称作i的回溯,正是由于i的不断回溯,这种算法的复杂度在最坏情况下达到了O(n*m),效率显然不能使人满意。
KMP算法的精髓就在于去掉了i的回溯,使得效率达到了O(n + m)。
举两个例子:
1.s[i] != p[j+1]
text = "abacbad"
pattern = "abad"
第四位出现失配,此时i = 3,j = 2.注意如果匹配成功则i,j都为3,但因为失配所以
j不能加1,j仍为失配位的前一位即2.
令 j = next[j] = 0,之后比较text[3] 与 pattern[1]......
2.j == -1
text = "abcxyz";
pattern = "abcd"
第四位出现失配,此时i=3,j=2,i原本应该不动等待j的跳跃,但由于模式串abcd中
无相同前后缀,故只好令i前进1位,重新从头匹配。
C代码如下:
const int maxn = 100;
int next[maxn];
void GetNext(char *p,int len)
{
int j = -1;
next[0] = -1;
for(int i = 1; i < len; i++)
{
while( j != -1 && p[i] != p[j+1])
j = next[j];
if(p[i] == p[j+1])
++j;
next[i] = j;
}
}
int KMP(char *s,char *p)
{
int j = -1;
int n = strlen(s);
int m = strlen(p);
GetNext(p,m);
for(int i = 0; i < n; i++)
{
while(j != -1 && s[i] != p[j+1]) //注意这里为什么要要求j不等于-1呢?因为j等于-1就不需要移动j了,j等于-1说明模式串需要重新从头匹配,
j = next[j]; //它刚好对应于p[j+1]=p[-1+1]=p[0],因此注意j==-1时只需要i++即可。
if(s[i] == p[j+1])
j++;
if(j == m-1)
return i - j;
}
return -1;
}
Python代码:
def GetNext(p):
j,p_len = -1,len(p)
nextv = [-1] * p_len
for i in range(1,p_len):
while j != -1 and p[i] != p[j+1]:
j = nextv[j]
if p[i] == p[j+1]:
j += 1
nextv[i] = j
return nextv
def KMP(s,p):
j,nextv = -1,GetNext(p)
for i in range(len(s)):
while j != -1 and s[i] != p[j+1]:
j = nextv[j]
if s[i] == p[j+1]:
j += 1
if j == len(p) - 1:
return i - j
return -1
nextval数组
若:
pattern = "ababab",text = "ababacab"
则:
next = {-1,-1,0,1,2,3}
当i = 5,j = 4时,'c' != 'b',失配;
j = next[j] = 2,i = 5. 'c' != 'b',失配;
j = next[j] = 0,i = 5. 'c' != 'b',失配;
j = next[j] = -1. j == -1,i++;
从这个例子中我们可能已经发现,对于一个模式串来说,它可能存在以下等式:
pattern[j+1] == pattern[next[j]+1] == pattern[next[next[j]]+1] == ......
这样的话,如果第j+1位失配,那么它的第next[j]+1位肯定也会失配,它的next[next[j]]+1位肯定也会失配,既然如此,何不把这些多余的工作全部省去呢?直接令j一步到底,这样就得到了next数组的优化——nextval数组。
要想让程序跳过无意义的回退,需满足pattern[i+1] != pattern[next[i]+1],而在普通的GetNext函数中,在令next[i] = j之前,j已经指向原先的next[i],因此原先的满足条件即变为pattern[i+1] != pattern[j+1]。
代码如下:
void GetNextval(char *p,int len)
{
int j = -1;
nextval[0] = -1;
for(int i = 1; i < len; i++)
{
if(j != -1 && p[i] != p[j+1]) //这里因为j = nextval[j]最多只会进行一次,因此可用if代替while
j = nextval[j];
if(p[i] == p[j+1])
j++;
if(j == -1 || p[i+1] != p[j+1])
nextval[i] = j;
else
nextval[i] = nextval[j];
}
}