KMP算法
KMP算法
模式串匹配算法,在一个主串(文本串s)中查找子串(模式串p)第一次匹配的位置
算法两个关键操作
- 根据模式串建立next数组
- 根据next数组进行子串匹配
1 Brute-Force算法(暴力求解)
两层for循环,遍历主串,从当前位置出发检查是否能与子串完全匹配。
如果匹配失败,检查主串下一个位置开始是否与子串匹配。
时间复杂度O(m*n)
没有利用到上一次匹配失败的信息以提升算法效率
2 KMP算法理解
Key Point:当匹配失败时,匹配失败位置之前的部分主串和子串是匹配的,利用这个信息,跳过中间不可能匹配成功的若干情况,以提升算法效率。
如果用上这个信息?
若已经匹配的部分有相同的前后缀,可以直接把子串移动多个位置,主串不动,使得子串的前缀和主串刚刚已经匹配部分的后缀相对应,继续比较主串和子串。
若相同的前后缀长度为已匹配部分的最大值,即最大相同前后缀,此时,最大限度的跳过了中间不可能匹配成功的情况,同时不会漏掉可能匹配成功的情况。
next数组:next[i]为模式串0~i最长相同前后缀长度(下标从0开始)
3 求next数组
难理解
初始next[0] = 0,串只有一个字符时,没有前缀后后缀,故最大相同前后缀长度为0
如果next[0], next[1], ... next[x-1]均已知,那么如何求出 next[x] 呢?
首先已经知道了next[x-1],即x之前的模式串的最大相同前后缀长度,设now = next[x-1]
则now即为最长相同前缀之后的一个字符,x为最长相等后缀之后的一个字符
-
若p[now] = p[x]
此时是最佳的情况,即最长相等前后缀在next[x-1]的基础上增长1位,next[x] = now + 1
如下图所示:
-
若p[now] != p[x]
此时不是最佳情况,要检查次优情况,缩短当前最长相同前后缀长度
注意到此时now-1=2,说明子串A最长相同前后缀长度为2,ab,子串A=子串B
故子串A前缀ab,与子串B后缀ab为次优最长前后缀长度,更新now,再检查p[now]==p[x]
如下图所示:
实现代码:
void buildNext(int *next, char *p, int lenp){
next[0] = 0; //初始next[0] = 0
int now = 0; //now = x-1
int x = 1; //初始x = 1
while(x < lenp){ //递推求next[x]
if(p[x] == p[now]){ //p[x]=p[now]则在原来最大前后缀长度的基础上加1
next[x++] = ++now;
// 等价于
// now++;
// next[x] = now;
// x++;
}
else{ //p[x]!=p[now]
if(now == 0){ //now=0,不能再缩小,next[x] = 0
next[x++] = 0;
}
else{ //缩小now
now = next[now-1];
}
}
}
}
4 KMP算法实现
构建好next数组之后,使用next数组进行匹配的过程中,根据失配位置和next数组的指示调整模式串指针,主串指针不回退。
匹配过程中的所有可能情况如下:
-
当前主串指针和模式串指针指向的字符相同(当前字符匹配成功)
两个指针均后移一位
-
当前字符匹配不成功
-
模式串第一位就匹配失败(p[0]适配)
模式串指针不动,主串指针后移一位
-
除p[0]之外的失配,模式串指针j(j != 0)
主串指针不动,模式串指针设为next[j-1]
-
图示理解如下
实现代码
int kmp(char *s, int lens, char *p, int lenp){
int *next = (int *)malloc(sizeof(int) * lenp);
buildNext(next, p, lenp);
int i = 0; //主串指针
int j = 0; //模式串指针
while(i < lens){ //遍历主串
if(s[i] == p[j]){ //当前字符匹配成功
i++;
j++;
}
else{ //失配
if(j == 0){ //p[0]失配,子串指针不动,主串指针后移1
i++;
}
else{ //非p[0]失配,根据next数组调整模式串指针
j = next[j-1];
}
}
if(j == lenp){ //成功匹配模式串
return i - j; //返回主串中匹配成功的起点
}
}
return -1; //匹配失败
}