KMP Search Algorithm

update Jun 4, 2017

KMP 的基本思想就是对O(n^2) 的brute force的匹配算法进行改进,改进的办法是对pattern字符串进行预处理,生成一个next数组,存放当前位置字符失配之后pattern指针要跳到的位置。

如果我们已经得到了next[]数组

我们需要进行匹配,匹配的过程非常简单。维持两个指针i , ji 对应source字符串,j 对应pattern。每次失配后 i 不动,移动 j 指向next[j] 位置的pattern character, 继续匹配。(这里所讲只是一个思路,实际上应该指向的是next[i - 1],因为这里的next数组指的其实只是当前公共前后缀最大长度,而非其他文章中所说的左移一位,然后把next[0] 设为 -1 的版本。我认为我的方法更易理解和实现,而其他文章中的方法或许效率更高,但毕竟是优化后的,不容易记忆。)匹配代码如下:

    public int strStr(String source, String target) {
        if (source == null || target == null) return -1;
        if (target.length() == 0) return 0;
        int[] next = getNextArr(target);
        // start to do kmp search
        int i = 0, j = 0;
        while (i < source.length() && j < target.length()) {
            if (source.charAt(i) == target.charAt(j)) {
                i++;
                j++;
            } else {
                if (j == 0) i++;
                else j = next[j - 1];
            }
        }
        return j == target.length() ? i - j : -1;
    }

下面我们来看如何获得next[] 数组

这个next[] 数组的本质是当前位置之前段 pattern有一定长度相同前缀和后缀,而数组中所存储的就是这个长度。于是我们从这里出发,可以 set next[0] to 0, 然后令 i = 0, j = 1 , 然后用ij右移进行匹配。这其实就是kmp算法本身的思想的应用,用pattern自己匹配自己。详情见下方的code。

    public static void main(String[] args) {
        char[] p = "abcgabcfabcgabcg".toCharArray();;
        int[] next = new int[p.length];
        next[0] = 0;
        int i = 0, j = 1;
        while (j < next.length) {
            if (p[i] == p[j]) {
                next[j++] = ++i;
            } else {
                if (i == 0) next[j++] = 0; // 如果最左边的都不匹配,则只把j+1继续匹配。
                else i = next[i - 1]; // 如果在中段失配,则尝试更短的同样前缀去匹配,关键步骤,
                                      // 解释见下面。
            }
        }
        Arrays.stream(next).forEach(System.out::println);
    }
    // output : 0000123012345674

上面的code用了一个比较有说服力的 example:

a

b

c

g

a

b

c

f

a

b

c

g

a

b

c

g

0

0

0

0

1

2

3

0

1

2

3

4

5

6

7

4

注意最右边g对应的4. 当 i next[7] 失配之后,我们让 i = next[i - 1] i = next[6] = 3 , 继续让 i j 比较,发现 p[i] == p[j] (p[3] = p[15]), 所以 next[j] = ++i == 3 + 1 = 4 。

一些同学可能会觉得这里的 i = next[i - 1] 很不好理解,起初我也觉得费解,所以在这里解释一下。要想理解这一步,首先要看 i 、i - 1 next[i - 1] 三者之间的关系(有点绕哈哈):

  • i : 当前需要和 j 匹配的字符的index,其实也是当前最大公共前缀后面的第一个需要匹配的字符的index。

  • i - 1 : 当前需要匹配字符之前最大公共前缀的长度。

  • next[i - 1] : 前一个最大公共前缀的长度,因为index是 0-based,所以next[i - 1] 也是上一个最大公共前缀之后的第一个需要匹配的字符的index。

现在我们来用一句话解释。因为当前最大长度公共前缀后面的第一个字符 i 失配,我们需要考虑上一个公共前缀后面的第一个字符也就是 next[i - 1], 看它是否和 p[ j ] 匹配.

(如果理解不了,就写出这个例子,一定可以理解。)

Next[] 数组的优化

可以发现,依照上述方法构造的next[] 数组是有缺陷的,比如下面这个pattern:

Pattern

a

b

c

d

a

b

c

e

a

b

c

f

a

next(before)

0

0

0

0

1

2

3

0

1

2

3

0

1

next (optimized)

0

0

0

0

0

0

3

0

0

0

3

0

1

Index

0

1

2

3

4

5

6

7

8

9

10

11

12

当p[10] 失配时,j 会移动到 next[j - 1] = next[9] = 2 , p[2] p[10] 都是 c,所以必然继续失配。为了避免这种不必要的情况发生,我们可以检查 if (p[j] == p(next[j - 1])) { next[j - 1] = next[next[j - 1]] } , 结果如表中next(optimized)所示。优化之后的code:

    private int[] getNextArray(String pattern) {
        char[] p = pattern.toCharArray();
        int[] next = new int[p.length];
        next[0] = 0;
        int i = 0, j = 1;
        while (j < p.length) {
            if (p[i] == p[j]) {
                next[j] = i + 1;
                if (p[j] == p[next[j - 1]]) { // 改动在这里,还可以更加简化,
                                              // 但这样写容易记忆,因为和思路一致。
                    next[j - 1] = next[next[j - 1]];
                }
                i++;
                j++;
            } else {
                if (i == 0) next[j++] = 0;
                else i = next[i - 1];
            }
        }
        return next;
    }

update Jul 6, 2017

最后再看一下O(n^2)的 StrStr 的实现,在匹配string比较短的时候,这种方法速度可能会更快。

    // java
    public int strStr(String haystack, String needle) {
        if (needle.length() == 0) return 0;
        for (int i = 0; ; i++) {
            for (int j = 0; ; j++) {
                if (i + j == haystack.length()) return -1;
                if (haystack.charAt(i + j) != needle.charAt(j)) break;
                if (j == needle.length() - 1) return i;
            }
        }
    }