KMP算法通俗易懂版

KMP算法是由Donald Knuth、James H.Morris和于1977年联合发明的,用来处理字符串匹配问题。

【题目描述】:对给定两个字符串str和substr,如果字符串str中含有子串substr,则返回substr在str中的开始位置,不含有则返回-1。

在介绍KMP算法之前,我们先来看普通解法(朴素模式匹配)怎么做。
最普通的解法是从左到右遍历str的每一个字符,然后看如果以当前字符作为第一个字符出发是否匹配出 substr,这里从数组下标为0开始。假设 str=“bcabcababcbabcabba”,substr=“abcabb”。
1.substr第0位字符不匹配,说明从str[0]出发进行匹配是不行的,后移1位,从str[1]出发进行匹配

bcabcababcbabcabba
abcabb

2.substr第0位字符不匹配,后移1位

bcabcababcbabcabba
abcabb

3.substr第5位字符不匹配,后移1位

bcabcababcbabcabba
abcabb

4.substr第0位字符不匹配,后移1位

bcabcababcbabcabba
abcabb

如此循环,直到匹配成功

普通解法的时间复杂度较高,假设字符串str和substr长度分别为N和M,从每个字符出发时,匹配的代价都可能是O(M),一共有N个字符,所以整体的时间复杂度为O(N×M)。普通解法的时间复杂度这么高,是因为每次遍历到一个字符时,检查工作相当于从无开始,之前的遍历检查不能优化当前的遍历检查。

比如在第3步,我们已经知道"abcab"这5个字符是匹配的,而普通解法没有利用到这一事实,只是简单的将substr串后移;KMP算法思想就是利用这一事实,减少重复比较的次数,提高算法效率。

KMP算法是怎么实现的这一点呢?他在此使用了一个叫做 “部分匹配表” 的数据结构,如下图所示,这张表是怎样求得的在后面会讲到。

substrabcabb
部分匹配值000120

在进行到第3步时,采用KMP算法,利用部分匹配表计算substr向后移动的位数:
3.substr第5位字符’b’不匹配,查表得到部分匹配值为0,说明str当前不匹配的位置应该与substr的第0位字符进行下一次匹配,即substr向后移动5位(=已匹配串"abcab"长度5 - 查表得到的部分匹配值0)

bcabcababcbabcabba
abcabb

4.substr第3位字符不匹配,查表得部分匹配值为1,substr向后移动2位(=已匹配串"abc"长度3 - 查表得到的部分匹配值1)

bcabcababcbabcabba
abcabb

5.substr第0位字符不匹配,后移1位

bcabcababcbabcabba
abcabb

6.substr第0位字符不匹配,后移1位

bcabcababcbabcabba
abcabb

7.substr第0位字符不匹配,后移1位得到在str匹配的初始位置11

bcabcababcbabcabba
abcabb

部分匹配表,也就是我们常说的next数组,它的求解过程是怎样的呢?首先我们需要了解两个概念"前缀"和"后缀"。

"前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。

如,对于字符串"abcab",他所有的前缀为{“a”,“ab”,“abc”,“abca”},后缀集合为{“b”,“ab”,“cab”,“bcab”}

"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度

因此"abcab"部分匹配值等于2,我们不难能手动计算出substr的部分匹配表(下标0位置默认部分匹配值为0)。

【算法实现】首先给出朴素模式匹配的算法,这里只用到了一个双重循环,简单粗暴,容易理解

    public static int naive(char str[], char substr[]) {
        int i, j, k = 0;//k记录下一轮比较的初始位置
        for (i = k; i < str.length; ) {
            for (j = 0; j < substr.length; ) {
                if (str[i] == substr[j]) {
                    ++i;
                    ++j;
                } else {
                    i = ++k;
                    break;
                }
            }
            if (j == substr.length)
                return k;
        }
        return -1;
    }

为了便于与KMP算法对照,将朴素模式匹配算法改造成如下形式,与上述代码的原理别无二致

    public static int naive2(char str[], char substr[]) {
        int i = 0, j = 0, k = 0;
        while (i < str.length && j < substr.length) {
            if (str[i] == substr[j]) {
                ++i;
                ++j;
            } else {
                i = ++k;
                j = 0;
            }
            if (j == substr.length)
                return k;
        }
        return -1;
    }

然后这是KMP算法求解过程,可以与上述朴素模式匹配算法对照来看,可以看出KMP算法:
1.需要next数组
2.不需要变量k来记录下一轮比较的初始位置,因为i不需要回溯
3.返回值问题,用i - substr.length来表示与模式串substr匹配的首字符所在位置

    public static int kmp(char str[], char substr[], int next[]) {
        int i = 0, j = 0;//因为i不需要回溯,所以变量k不需要
        while (i < str.length && j < substr.length) {
            if (j == 0 || str[i] == substr[j]) {
                ++i;
                ++j;
            } else {
                j = next[j];
            }
            if (j == substr.length)
                return i - substr.length;//与模式串匹配的首字符所在位置
        }
        return -1;
    }

然后是next数组的代码实现

    public static void getNext(char substr[], int next[]) {
        int i, k;//k为substr[0..i]最大前后缀长度
        next[0] = 0;
        for (i = 1, k = 0; i < substr.length; ++i) {
            while (k > 0 && substr[i] != substr[k])
                k = next[k - 1];
            if (substr[i] == substr[k])
                k++;
            next[i] = k;
        }
    }


版权声明:本文为chen2434159177原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。