一、问题的由来

我们会在面试或者日常“搬砖”过程中遇到这类问题:有一个文本串S(比如“ababbbaccdddmmd”),有一个模式串Q(比如“baccdd”),判断模式串Q是否是S的字串,如果是返回Q在S中的起始位置,如果不是返回-1。我们脑海里第一个思路就是循环遍历,如果当前字符匹配成功就继续匹配下一个,否则S中的标记向后移动一位,Q的标记回到最开始,就会有如下代码:

public static int kmpMatch(char[] chars, char[] ch) {
        //S的长度
        int sLen = chars.length;
        //Q的长度        
        int qLen = ch.length;
        int i = 0, j = 0;
        while (i < sLen && j < qLen) {
            if (chars[i] == ch[j]) {
                i++;
                j++;
            } else {
                //这里是把i回到这一轮的开始位置后再向后移动一位
                i = i - j + 1;     // i - ( j - 1 )
                j = 0;
            }
        }
        if (j == qLen) return i - j;
        else return -1;
    }

这个解法是正确的,但是我们是否在每次匹配失败的时候都需要另Q的标志位都回到0呢?显然是不需要,这就引入了KMP算法。

二、KMP算法

KMP算法和常规的暴力算法的区别就在于当S的字符和Q的字符不匹配时候的标记符号的移动,暴力解法是另i = i - j + 1,j = 0。在KMP算法中的核心思想是找到字符串Q中的前缀和后缀中的最长公共长度,让i不变,j移动最长公共字串的长度,这样就可以有效的避免了重复字符的移动,所以只需要求出j的吓一跳的数组就可以了我这里说的比较简单,我特意找了我当时学习时的一篇博客供大家学习 KMP详解,回到本文,什么是最长公共前后缀的长度呢? 

以下说明以“ABCDABD”为例进行说明:

    -"A"的前缀和后缀都为空集,共有元素的长度为0;

  -"AB"的前缀为[A],后缀为[B],共有元素的长度为0;

  -"ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;

  -"ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;

  -"ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;

  -"ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;

  -"ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素长度为0。

所以就得到了这样一个数组next[7]{0,0,0,0,1,2,0},所以每次只需要另j移动next[k]位就可以了,k就是每次不匹配字符的位置。于是,代码就可以写成如下:

    private static int kmpMatch(char[] chars, char[] ch) {
        int sLen = chars.length;
        int pLen = ch.length;
        int[] next = getNextArray(ch);
        for (int i : next) {
            System.out.print(i+" ");
        }
        int i = 0, j = 0;
        while (i < sLen && j < pLen) {
            if (chars[i] == ch[j]) {
                i++;
                j++;
            } else {
                j = next[j];
            }
        }
        if (j == pLen) return i - j;
        else return -1;
    }

    public static int[] getNextArray(char[] sub) {
        int[] next = new int[sub.length];
        next[0] = -1;
        next[1] = 0;
        int k;
        for (int j = 2; j < sub.length; j++) {
            //System.out.println(j);
            k = next[j - 1];
            while (k != -1) {
                if (sub[j - 1] == sub[k]) {
                    next[j] = k + 1;
                    break;
                } else {
                    k = next[k];
                }
                next[j] = 0;
            }
        }
        return next;
    }

通过getNextArray函数就可以获得模式串Q的下一跳位置的数组,这样就得到了KMP算法的代码。如果还有什么不明白的可以留言,我会进行解答的。