近日查看上届“恒生杯”初赛的题目,有一道题需要用到KMP算法。KMP算法在《数据结构》课上学过,但是当时只听了个一知半解,也没有自己动手实践。写这篇文章既算是对自己以前学习态度的一种反思,也是填自己以前挖的大坑。
声明:本文参考了july的《从头到尾彻底理解KMP》
以及 http://www.cnblogs.com/dolphin0520/archive/2011/08/24/2151846.html
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth与V.R.Pratt和J.H.Morris同时发现,而Knuth正是《The Art of Computer programming》的作者高德纳。不过在介绍KMP算法之前,首先介绍一种大家都能想的到的字符串匹配算法——BF算法。
1.BF算法
BF即Brute Force,闻名知意,BF算法是一种低效的暴力算法。此算法的过程大家也很容易想到,将文本串S的第一个字母和模式串P的第一个字母相比较,如若相等,则比较S第二个字母和P第二个字母;否则,比较S第二个字母和P的第一个字母。依次比较下去,就能得到首次匹配成功的位置,或者S到达末尾,匹配不成功。该方法的核心是文本串S的下标在比较不相等时需要回溯,而模式串P下标归零。
举例说明:文本串S: abababc
模式串P: ababc
i表示文本串S的下标,j表示模式串P的下标。
i = 0 i = 1 i++,j++直到... i = 4 此时s[i] != p[j],i回溯,j归零 i = 1
abababc abababc abababc abababc
ababc ababc ababc ababc
j = 0 j = 1 j = 4 j = 0
s[i] != p[j]...回溯 i = 2 i = 2 i = 3 i = 4 i = 5 (成功)
abababc abababc abababc abababc abababc
ababc ababc ababc ababc ababc
j = 0 j = 1 j = 2 j = 3 j = 4
BF算法:
int BFMatch(char* s,char* p)
{
int i = 0;
int j = 0;
int slen = strlen(s);
int plen = strlen(p);
while(i < slen && j < plen)
{
if(s[i] == p[j])
{
i++;
j++;
}
else //i回溯,j归零
{
i = i - j + 1;
j = 0;
}
}
if(j == plen) //模式串到尾部,匹配成功
return i - j;
else return -1; //匹配失败
}
2.KMP算法
KMP算法则是省去了BF算法的一些步骤。每次匹配不成功时,保持s下标i不变,改变p下标j,这样使时间复杂度由O(mn)降到了O(m+n)。为了确定p下标j的值,引入next数组。next[j]表示p[0...j-1]中的前后缀最长公共元素长度。
next[]严谨定义如下:
next[j] = -1 , j = 0
next[j] = max(k) , p[0 ... k-1] = p[j-1-k ... j-1] 0< k < j
所以,当p为ababc,对应next[]数组为:
p : a b a b c
next : -1 0 0 1 2
如果在匹配过程中,出现s[i] != p[j] 情况,如果next[j] >= 0,保持i不变,j = next[j];如果next[j] == -1,则令i++,j++。匹配过程如下,对比上文BF算法过程,可知KMP算法省去了哪些步骤。
i = 0 i = 1 i = 2 i = 3 i = 4
a b a b a b c a b a b a b c a b a b a b c a b a b a b c a b a b a b c
a b a b c a b a b c a b a b c a b a b c a b a b c
j = 0 j = 1 j = 2 j = 3 j = 4(j = next[j] = 2)
i = 4 i = 5 i = 6 (匹配成功)
a b a b a b c a b a b a b c a b a b a b c
a b a b c a b a b c a b a b c
j = 2 j = 3 j = 4
KMP算法巧妙地利用了模式串p的前后缀相同部分,减少了没有必要的比较。但是next数组该如何求取呢?我们可以利用递推的方法。
已知next[0] = -1,next[j] = k 即 p[0...k-1] == p[j-1-k ... j-1] .
若p[k] == p[j],即 p[0...k] == p[j-1-k .. j],则next[j+1] = k+1 .
若p[k] != p[j],把它看作后缀和前缀在进行模式匹配,根据上文知,匹配失败时,k = next[k] .如果一直匹配失败直到k == -1,那么前后缀最长公共元素长度为0,令next[j+1] = 0 .
注意:求取next[1]时,虽然p[0] == p[0],但是大家都令next[1] = 0。如果next[1] = 1的话,KMP匹配会陷入死循环。
求取next数组:
void GetNext(char* p,int next[])
{
int plen = strlen(p);
int k = -1;
int j = 0;
next[0] = -1;
while(j < plen - 1)
{
if(k == -1 || p[j] == p[k])
{
k++;
j++;
next[j] = k; //相当于next[j+1] = k+1
}
else
k = next[k];
}
}
KMP算法:
int KMP(char* s,char* p)
{
int slen = strlen(s);
int plen = strlen(p);
int i = 0;
int j = 0;
while(i < slen && j < plen)
{
if(j == -1 || s[i] == p[j])
{
i++;
j++;
}
else
j = next[j]; //i不变,j改变
}
if(j == plen) //匹配成功,返回s中匹配初始坐标
return i - j;
else return -1;
}