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
第二趟匹配,i=1,j=0匹配失败,i回溯到2,j回溯到0
第三趟匹配,i=2,j=0失败,i回溯到3,j回溯到0
第四趟匹配,i=8,j=5,T中字符全部比较完毕,匹配成功
这是最简单的思路,但是问题是,两个字符串都要回溯,直接导致复杂度太高,时间复杂度是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。
代码如下:
【问题分析】
首先,比较暴力的方法是,同时遍历S和T,如果某个位置的匹配不成功,那么就回溯,字符串S回溯到上一次起始位置后一个,字符串T回溯到0位置。示意图如下:
S:abcabcacb
T:abcac
第一趟匹配,i=4,j=4时失败,i回溯到1,j回溯到0
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
a | b | c | a | b | c | a | c | b |
↓ | ↓ | ↓ | 不匹配 | |||||
a | b | c | a | c |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
a | b | c | a | b | c | a | c | b |
不匹配 | ||||||||
a |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
a | b | c | a | b | c | a | c | b |
不匹配 | ||||||||
a |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
a | b | c | a | b | c | a | c | b |
↓ | ↓ | 4000↓ | ↓ | ↓ | ||||
a | b | c | a | c |
下面就是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; } }