您的位置:首页 > 其它

KMP字符串匹配算法

2017-08-19 22:17 253 查看
【问题】给定两个字符串S和T,在主串S中查找子串T,如果找到就返回匹配的起始位置下标

【问题分析】

首先,比较暴力的方法是,同时遍历S和T,如果某个位置的匹配不成功,那么就回溯,字符串S回溯到上一次起始位置后一个,字符串T回溯到0位置。示意图如下:

S:abcabcacb

T:abcac

第一趟匹配,i=4,j=4时失败,i回溯到1,j回溯到0

012345678
abcabcacb
不匹配
abcac
第二趟匹配,i=1,j=0匹配失败,i回溯到2,j回溯到0

012345678
abcabcacb
不匹配
a
第三趟匹配,i=2,j=0失败,i回溯到3,j回溯到0

012345678
abcabcacb
不匹配
a
第四趟匹配,i=8,j=5,T中字符全部比较完毕,匹配成功

4000
012345678
abcabcacb
abcac
这是最简单的思路,但是问题是,两个字符串都要回溯,直接导致复杂度太高,时间复杂度是O(n*m)。这个算法叫朴素的模式匹配算法,简称BF算法。这个代码就不贴了,比较简单。

下面就是KMP算法的思路了。

在KMP算法中,我们只需遍历字符串S,而不需要回溯,需要回溯的是字符串T,这样大大降低时间复杂度。



这是上面的两个字符串,可以看到当 i =3, j =3时,匹配失败,但是在这个时候, i 位置前面的 j 个数,S 和 T都是匹配成功的,所以我们没有必要再匹配这一段,因此指向 S 的 i 不动,j 回溯。我们假设 j 回溯到位置 k 。位置 k 由 j 位置的 next 值 next[j] 决定。

next值指什么?就是指在 j 位置前面不含 j 的最长前缀和最长后缀相等的长度。最长前缀指从0到 j-2 位置的而且必须以0开头的子数组,最长后缀就是从1到 j-1位置而且必须以j-1结尾的子数组。

KMP算法的关键部分就在于next数组,也有很多人不理解Next数组到底是怎么算出来的,下面画图为例。



首先我们规定,next[0]=-1,next[1]=0。因为0位置前面没有任何字符,所以是-1,1位置前面不满足前缀后缀成立的条件即前缀右边界小于后缀右边界,前缀左边界小于右缀左边界,所以是0。

由上图可知next[k]=b+1,此时我们要求出next[k+1]。

我们比较T[b+1]和T[k]是否相等,如果相等,那么next[k+1]=next[k]+1。当在K位置时,因为0~b是k的最长前缀,图中间的红线到k-1是最长后缀,如果b+1位置跟k位置相等,那么最长前缀长度就增加了1,由于最长后缀右边界是k,所以k+1的最长前缀和最长后缀长度就是next[k]+1。

如果不相等呢?关键的地方来了。



那就跳到0~next[k]的区域,比较 b+1 位置的字符与 k 位置的字符是否相等,若相等就next[k+1]=next[b+1]+1。可能有人会有疑惑为什么直接+1。因为我们是选取的0~b+1部分,由于0~b与k-1-b~k-1相同,所以0~a对应的后缀就是k-1-a~k-1。他们右边界都往后移一位而且都相同,因此next[k+1]=next[b+1]+1。如果b+1位置字符与k位置字符还是不相等,就继续跳到前缀找前缀,直至next值为-1或者相等,如果Next值为-1,那么就给next[k+1]赋值0,因为这时已经找不到最长前缀了。

以上就是如何计算短字符串T对应的next数组。

得到了Next数组,就可以开始KPM算法的流程了:

如果S[i]==T[j],继续比较后面的字符

否则,将下标j回溯到next[j]位置

如果j==-1, i 和 j 均右移一位,进行下一趟比较

如果T中所有字符都比较完毕,则返回本趟匹配的开始位置,否则返回0。

代码如下:

public class Main {
public static void main(String[] args) {
String strshort = "ababa";
String strlong = "abcabcababaccc";
System.out.println(KMP(strshort.toCharArray(), strlong.toCharArray()));
}

/* KMP算法 */
public static int KMP(char[] chs1, char[] chs2) {
int l = 0;
int[] next = getNextArray(chs1);
int s = 0;
while (l < chs2.length && s < chs1.length) {
if (chs1[s] == chs2[l]) {
s++;
l++;

} else {
s = next[s];
if (s == -1) {
l++;
s++;
}
}

}
if (s == chs1.length)
return l - s;
else
return -1;
}

/* 短字符串的next数组 */
public static int[] getNextArray(char[] chs) {
if (chs.length == 1)
return new int[] { -1 };
int[] next = new int[chs.length];
next[0] = -1;
next[1] = 0;
int pos = 2;
int pre = 0;// pos位置前一个字符(pos-1)的next值,也就是最长前缀的长度,对应的最长前缀的右边界为pre-1
while (pos < next.length) {
if (chs[pos - 1] == chs[pre]) {
// 如果pos-1位置的字符与pos-1最长前缀右边界后面一个字符相同
// 那么next[pos]的值就是next[pos-1]+1
next[pos++] = pre + 1;
pre = next[pos - 1];
} else {
if (pre <= 0) {
next[pos++] = 0;
} else {
pre = next[pre];
}
}
}
return next;
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: