什么是KMP算法?

KMP算法是一种改良的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因而人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的外围是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到疾速匹配的目标。

暴力搜索算法实现


请问,在字符串 T 中是否蕴含 P 的 "ababc"?

咱们能够从第一个字符开始比对,如下图:

在第四次比对的时候,咱们发现 T 字符串和 P 字符串并不统一。
咱们将字符串 P 整体后移一位从新进行比照。

第一次比对咱们就发现 T 字符串和 P 字符串比对不上。所以咱们须要持续后移 P 字符串进行比照。

以此类推,当所有字符能匹配上则阐明匹配胜利,如果匹配到 T 的开端都没有胜利的话则失败,此算法效率很低。

KMP算法实现


请问,在字符串 T 中是否蕴含 P 的 "ababc"?

第一步:
在学习KMP算法之前,咱们须要先理解前缀表(Prefix Table)。

咱们先做的就是找到字符串长度较短的字符串,也就是字符串 P。

a

a b

a b a

a b a b

一共有四个前缀。

第二步:

咱们须要找出最长公共前后缀并且比原始字符串短。

这句话有点不太好了解,咱们应用a b a b来举例子。

咱们能够看到 a b a b 最长前缀就是 a b a,最长后缀就是 b a b。

然而咱们发现这两个字符串显著是不一样的。

3个字符的前后缀显著不同,所以咱们应用2个字符前后缀进行匹配看看。

咱们发现 前缀是 a b 后缀也是 a b 。这样咱们就晓得最长公共前后缀是2。

而后咱们持续往上看 a b a 很显著它的公共前后缀就是1。如果咱们用2来看的话就是 ab 和 ba 很显著是不同的。

ok。再往上就是 a b 很显著,1是不匹配的。所以最长公共前后缀是0。 a 也一样。

咱们看下最终是怎么的。

咱们进行整体向右挪动一位,而后初始值赋为 -1 能够失去如下后果:

ababa
-10012

这样咱们去进行和 T 字符串进行比对,当比对到下标为3时,咱们发现 a 和 b不同匹配失败。

这个时候咱们应该怎么做?

咱们发现在 P 数组中下标为3的 b 匹配谬误,而 b 的前缀表是 1,咱们须要将 P 数组下标为 1 的地位 挪动 T 数组匹配失败的地位。

然而咱们发现匹配还是谬误的,咱们能够晓得 b 的 前缀表是 0 所以咱们让下标为0的地位整体挪动到 T 数组匹配失败的地位。

咱们从新进行匹配,发现第一个 a 相等,第二个 b 和 c 不相等。

咱们继续移动,b 的前缀表是 0 所以咱们让下标为0的地位整体挪动到 T 数组匹配失败的地位。然而还是 a 和 c 匹配失败。

然而咱们晓得 a 的前缀表是 -1 也就是一个空的字符串,咱们将空的字符串对准匹配失败的地位,等同于-1就是整体右移一位

这个时候咱们持续进行匹配,发现曾经匹配胜利了!

暴力搜寻和KMP区别

举一个例子:

如果咱们使用暴力搜寻的话咱们会发现咱们须要将 P 字符串始终后移一位,须要四次后移才可匹配胜利。

如果咱们应用KMP来解决的话,还是首先咱们须要晓得前缀表是多少,如下图:

aaaab
-10123

而后咱们进行比对。

前缀表为3匹配失败,将数组下标为3挪动至此,持续进行比照。

须要留神的是,咱们不须要再重头开始比照,只须要从下标为3的地位开始匹配即可

前面其实就是大同小异,持续后移匹配即可。

总结

由此咱们能够晓得,暴力搜寻每次挪动后都要从第一位开始从新匹配,而咱们用KMP的话咱们不须要再重头开始比照能够省去大量工夫。

Java代码实现KMP算法

应用Java代码如何先失去前缀表。

    private static void buildPrefixTable(char[] pattern, int[] prefix) {        // 比照指针下标        int len = 0;        // 第一位不须要进行比对因为必定是0        prefix[0] = 0;        //因为第一位不须要进行比对,咱们从1开始        int i = 1;        //最初一位不须要比拟因为咱们须要的前缀表是比原始字符串短        int length = prefix.length -1;        while (i < length) {            if (pattern[i] == pattern[len]) {                //匹配胜利,自增持续匹配下一位字符                len++;                prefix[i] = len;                i++;            } else {                if (len > 0) {                    //取得前一位的前缀                    len = prefix[len - 1];                } else {                    //没有找到只能是0,i++持续匹配下一位                    prefix[i] = len;                    i++;                }            }        }        //整体后移一位,将第一位批改为-1        for (int j = prefix.length - 1; j > 0; j--) {            prefix[j] = prefix[j - 1];        }        prefix[0] = -1;    }

整体逻辑代码如下:

    public static void main(String[] args) {        String T = "ABABABCABAABABABAB";        String P = "ABABCABAA";        char[] text = T.toCharArray();        char[] pattern = P.toCharArray();        kmpSearch(text, pattern);    }    private static void kmpSearch(char[] text, char[] pattern) {        int m = text.length;        int n = pattern.length;        int[] prefix = new int[n];        buildPrefixTable(pattern, prefix);        // 字符串 text 的指针下标        int i = 0;        // 字符串 pattern 的指针下标        int j = 0;        while (i < m) {            if (j == n - 1 && text[i] == pattern[j]) {                System.out.println("找到了!开始下标为:" + (i - j));                j = prefix[j];                //如果未比拟的pattern长度已超出text剩下的长度则提前结束                if (n - j > m - i) {                    break;                }            }            if (text[i] == pattern[j]) {                //雷同的话自增匹配下一位                i++;                j++;            } else {                //不同的话就将前缀值当下标进行整体挪动                j = prefix[j];                if (j == -1) {                    //如果值为-1则阐明须要整体后移一位进行匹配                    i++;                    j++;                }            }        }    }    /**     * 构建前缀表     */    private static void buildPrefixTable(char[] pattern, int[] prefix) {        // 比照指针下标        int len = 0;        // 第一位不须要进行比对因为必定是0        prefix[0] = 0;        //因为第一位不须要进行比对,咱们从1开始        int i = 1;        //最初一位不须要比拟因为咱们须要的前缀表是比原始字符串短        int length = prefix.length -1;        while (i < length) {            if (pattern[i] == pattern[len]) {                //匹配胜利,自增持续匹配下一位字符                len++;                prefix[i] = len;                i++;            } else {                if (len > 0) {                    //取得前一位的前缀                    len = prefix[len - 1];                } else {                    //没有找到只能是0,i++持续匹配下一位                    prefix[i] = len;                    i++;                }            }        }        //整体后移一位,将第一位批改为-1        for (int j = prefix.length - 1; j > 0; j--) {            prefix[j] = prefix[j - 1];        }        prefix[0] = -1;        System.out.println(Arrays.toString(prefix));    }

输入后果:

[-1, 0, 0, 1, 2, 0, 1, 2, 3]找到了!开始下标为:2