一篇文章搞懂KMP算法

407 阅读5分钟

1. KMP算法解决的是什么问题

用于查找字符串A中是否包含子字符串B。

2. 为什么要用KMP算法

寻找字符串B是否在字符串A中,最简单暴力的方法就是,以A的第一个字符(简称为A-0,下同)为开头,B-0为开头,依次往后比对字符串B。 image.png

比对完全相等,返回,比对不相等, image.png

B回退到B-0,A回退到A-1,以A-1为开头,依次往后比对B Untitled.png

依次类推,直至匹配字符串B或者以字符串A的某个字符开头的字符串长度小于B的长度,结束比对。 image.png 假设字符串A,B的长度分别是n,m

上诉这种方法的时间复杂度为O(n*m)

而kmp算法,可以使时间复杂度达到O(n)

3.KMP算法是怎么实现的

其实原理跟暴力方法一样,也是要以某个字符为开头,依次往后比对,只不过在比对不相等时,处理方式有所优化,kmp算法不会将B直接回退到第一个字符,而是回退到B的某个特定字符,而A不会回退,直接保留在原地或者前进

流程实现:

a.先准备一个与字符串B等同长度的数组,称之为next数组,用于存储B所有字符位置之前的前缀和后缀相等的最长长度。

如图字符串B,index=2的位置,在它之前的字符串中,前缀和后缀相等的最长长度是1,即next[2]=1,index=3的位置长度为2,即next[3]=2。

1.png

2.png

b.开始比对

b.1 以A,B第一个字符开始比对,比对到不等的字符,如图,比对到index=5,字符不等,

3.png b.2 此时A保留在index=5的位置,而B回退到next[5]值的位置,假设为3,

4.png

b.3 然后以此为对比点,往后开始对比,如果不等,B再回退到next[3]值的位置,假设为0,

5.png

b.4 然后以此为对比点,往后开始对比,如果不等,由于此时B无法再退了,所以此时A开始前进,即以A-6为对比点,与B开始往后比对,

6.png

b.5 依此类推,直至匹配到字符串B或者以字符串A的某个字符开头的字符串长度小于B的长度,结束比对。

实质原理:

上诉步骤中,有没有这个疑问:为啥在比对不等的情况下,A可以保留在index=5的位置(而不是回退到它原本应该的比对点A-1),B回退到next[5]值的位置(而不是回退到起点B-0),实质是什么?

实质一:

其实B回退到next[5]值的位置,实质是B回退到了起点B-0,A从A-5回退了next[5]值,也就是回退到了A-2,以A-2为对比点,往后开始对比,

7.png

但因为B-0~B-2与A-2~A-5这一段是相等的(因为B-5的next值为3,也就是前缀长度为3和后缀长度为3的字符相等,即B-0~B-2==B-2~B-4,而因为上诉b.1步骤是对比到index=5才出现字符不等,所以,B-2~B-4==A-2~A-4,所以B-0~B-2==A-2~A-4),所以省去了对比,直接从B-3,A-5开始对比。 image.png

实质二:

但为啥以A-0开头对比失败后,直接跳过了A-1,直接以A-2为对比点,为什么可以跳过A-1的对比点。其实也是证明以A-1开头找不出B。

这里采用反证法证明一下,我们不妨假设以A-1开头可以找出B,即A-1~A-7==B-0~B-6

image.png

那么也就是说A-1~A-4长度为4的字符串,等于以B-0开头同等长度的字符串,也就是B-0~B-3==A-1~A-4, image.png 而又因为上诉b.1步骤是对比到index=5才出现字符不等,所以B-1~B-4==A-1~A-4,所以B-1~B-4==B-0~B-3,也就是B-5此时的前缀和后缀相等的最大长度为4,但我们b.2步骤说明了B-5的next值为3,互相矛盾,所以假设不成立,也就是以A-1开头找不出B,所以可以直接跳过A-1。

代码实现&注释解析(java):

public class KMP {
    /**
     * 主方法 查询字符串str中能否找出match字符串,如果找不出,返回-1,如果可以找出,返回匹配开始点
     *
     * @param str   字符串A
     * @param match 字符串B
     * @return
     */
    public static int getMatchStrStartIndex(String str, String match) {
        if (str == null || match == null || str.length() == 0 || match.length() == 0 || str.length() < match.length()) {
            return -1;
        }
        char[] strArr = str.toCharArray();
        char[] matchArr = match.toCharArray();

        //生成next数组
        int[] next = returnNextArr(matchArr);

        //字符串A指针
        int strIndex = 0;
        //字符串B指针
        int matchIndex = 0;

        //当A、B任意一个指针超过字符串长度时,比对完毕,退出循环
        while (strIndex < strArr.length && matchIndex < matchArr.length) {

            if (strArr[strIndex] == matchArr[matchIndex]) {
                //当A、B字符串字符相等,各自往后走一步
                strIndex++;
                matchIndex++;
            } else if (matchIndex == 0) {
                //当A、B字符串字符不等且B字符串指针为0时,退不了了,所以只能字符串A往后走
                strIndex++;
            } else {
                //当A、B字符串字符不等且B字符串指针不为0时,A指针保持原地,B指针退回到next[B指针位置]值处
                matchIndex = next[matchIndex];
            }
        }
        //当字符串B指针大于等于字符串B长度,说明字符串A可以找出字符串B,返回匹配开始点,否则返回-1
        return matchIndex >= matchArr.length ? strIndex - matchArr.length : -1;
    }

    /**
     * next数组
     * 数组含义:i位置之前,前缀和后缀相等的最大数量,如aaabaaabt,t所在位置值为4(aaab),而aaabaaab不算,因为跟t之前的字符串长度等长了
     * <p>
     * 人为规定0位置值为-1,1位置值为0
     * <p>
     * 数组创建思路:设x位置next数组值为5,求x+1位置next数组的值
     * 1.判断x位置的字符是否与5位置的字符相等,如果相等,x+1位置next数组的值为5+1=6
     * 2.如果不等,x位置跳到5的位置上,重复上面动作
     * 3,如果x已经来到0位置了还不等,那x+1位置next数组的值为0
     *
     * @param match
     * @return
     */
    public static int[] returnNextArr(char[] match) {
        if (match.length == 1) {
            return new int[]{-1};
        }
        int[] result = new int[match.length];
        result[0] = -1;
        result[1] = 0;
        int index = 2;
        int preResult = 0;
        while (index < result.length) {
            if (match[preResult] == match[index - 1]) {
                result[index++] = ++preResult;
            } else if (preResult != 0) {
                preResult = result[preResult];
            } else {
                result[index++] = 0;
            }
        }
        return result;
    }

    // 验证测试使用
    public static String getRandomString(int possibilities, int size) {
        char[] ans = new char[(int) (Math.random() * size) + 1];
        for (int i = 0; i < ans.length; i++) {
            ans[i] = (char) ((int) (Math.random() * possibilities) + 'a');
        }
        return String.valueOf(ans);
    }

    //验证测试
    public static void main(String[] args) {
        int possibilities = 5;
        int strSize = 20;
        int matchSize = 5;
        int testTimes = 5000000;
        System.out.println("test begin");
        for (int i = 0; i < testTimes; i++) {
            String str = getRandomString(possibilities, strSize);
            String match = getRandomString(possibilities, matchSize);
            //暴力方法
            int i1 = str.indexOf(match);
            //kmp算法
            int matchStrStartIndex = getMatchStrStartIndex(str, match);
            if (matchStrStartIndex != i1) {
                System.out.println("no!");
                System.out.println(str + "---" + match);
                break;
            }
        }
        System.out.println("test finish");
    }
}