KMP

267 阅读3分钟

前言

初次接触到kmp算法的人大部分都比较难以理解,所以有了这篇文章。 楼主看了(参考)下面这些大佬文章,也可以点过去看看。

kmp算法

简介

Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”,常用于在一个文本串S内查找一个模式串P的出现位置,这个算法由Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年联合发表,故取这3人的姓氏命名此算法。

先上代码

    /**
     *
     * @param s  字符串  BBC ABCDAB ABCDABCDABDE
     * @param p  匹配串  ABCDABD
     */
    public int kmp(String s, String p){
        // 获取next数组
        int[] next = kmpnext(p);
        char[] sarr = s.toCharArray();
        char[] parr = p.toCharArray();
        int slen = s.length();
        int plen = p.length();
        int i = 0, j = 0;
        while(i < slen && j < plen){
            // j=-1 在p中没有对应的字符
            if(j == -1 || sarr[i] == parr[j]){
                i++;
                j++;
            }else {
                j = next[j];
            }
        }

        if(j >= plen){
            // 返回头索引
            return i - j;
        }else{
            return -1;
        }
    }

    /**
     * @param p 匹配串
     * @return
     */
    private int[] kmpnext(String p){
        char[] parr = p.toCharArray();
        int[] next = new int[p.length()];

        next[0] = -1;
        int k = -1;
        int j = 0;
        while (j < p.length() - 1) {
            //p[k]表示前缀,p[j]表示后缀
            if (k == -1 || parr[j] == parr[k]) {
                ++k;
                ++j;
                next[j] = k;
            }else {
                k = next[k];
            }
        }
        return next;
    }

kmp原理

字符串s索引i,匹配串p索引j
①循环s,p,s[i] 与 p[j] 比较
②当s[i] == p[j],i,j都往后移一位
当s[i] != p[j],i索引不动,将j重置到next[j]1,继续比较s[i] 与 p[j],重复①-②步骤
③这样可能会数组越界,当next[0] == -1时2,即j变成-1时,代表s[i] 与 p[0]不等, 也将i,j都往后移一位

思想: 每当匹配过程中出现相比较字符不等时,不需要回退主串位置指针,而是利用已经得到的部分匹配结果将模式串向右滑动尽可能远的距离,再继续比较
当匹配串p[j]与主串s[i]不等时,因为前j个字符(p[0]-p[j-1])已经匹配成功,所以只要找到(p[0]-p[k-1]) = (p[j-k]-p[j-1])即前后缀最大相同数
--《软件设计师教程》

next数组

next定义:

next[j]={1j=0max{k0<k<j"P0...Pk1"="Pjk...Pj1"}0其他情况next[j]= \begin{cases} -1 & 当j=0时 \\ max\{ k | 0 < k < j 且 "P_0...P_{k-1}" = "P_{j-k}...P_{j-1}"\} \\ 0 & 其他情况 \end{cases}

next数组含义:代表当前字符之前的字符串中,有多大长度的相同前缀后缀

匹配串p索引j,k代表代表当前字符p[j]之前的字符串中,有多大长度的相同前缀后缀,当j == 0时j前面都没有字符,所以就给next[0] == -1,-1也有意义,就是他通过加1会回到0位置
含义先搞懂,代码才看得懂

①循环p,当k == -1 或 p[j] == p[k],k,j都往后移一位,next[j] = k, 表示有k个相同前后缀
②当k != -1 且 p[j] != p[k],k = next[k],继续循环。

重点来了,很多人不理解这句k = next[k],下面讲

思想:同kmp算法思想;前缀串p[k]与后缀串p[j]不匹配时,因为前k个字符(p[0]-p[k-1])已经匹配成功,所以只要找到(p[0]-p[k-1]) = (p[j-k]-p[j-1])即前后缀最大相同数,此时k 也就是 next[k]了,k既代表前缀串的索引,又代表多大长度的相同前缀后缀

示例:有匹配串 ABCDABD
过程如下图

next.png

当k != -1 且 p[j] != p[k],相当于k位置之前的字符串中,有多大长度的相同前缀后缀,查询这个就是取next数组next[k]的值,这个值的意义不用多说,把k跳到值的位置继续匹配

很多人解释这句,大多举例验证正确,没有解释为什么这样。看了上面的文字说明加图解,相信大家都能明白。解释有问题的或者楼主写错的欢迎在评论区留言,一起搞明白。

Footnotes

  1. next[j]是一个数组,也是kmp最核心的,下面讲怎么计算

  2. next数组第一个元素定义为-1