KMP字符串匹配算法

来源:互联网 发布:软件销售管理制度 编辑:程序博客网 时间:2024/05/29 07:00

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

【问题分析】
首先,比较暴力的方法是,同时遍历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

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

0 1 2 3 4 5 6 7 8 a b c a b c a c b 不匹配 a

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

0 1 2 3 4 5 6 7 8 a b c a b c a c b 不匹配 a

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

0 1 2 3 4 5 6 7 8 a b c a b c a c b ↓ ↓ ↓ ↓ ↓ a b c a c

这是最简单的思路,但是问题是,两个字符串都要回溯,直接导致复杂度太高,时间复杂度是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算法的流程了:

  1. 如果S[i]==T[j],继续比较后面的字符
  2. 否则,将下标j回溯到next[j]位置
  3. 如果j==-1, i 和 j 均右移一位,进行下一趟比较
  4. 如果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;    }}
原创粉丝点击