KMP算法-简单易懂会写了

114 阅读5分钟

背景介绍:KMP算法背景

算法复杂度o(m+n)字符串匹配算法

本文以匹配字符串为ABCABCD ,搜索字符串为ABCABCABCD 为例

请观察:搜索字符串ABCABCABCD 与匹配字符串ABCABCD

匹配到第七个字符A时,与匹配字符串第七个字符D不相等,暴力匹配算法从搜索字符串第二个字符开始重新匹配,未利用到匹配字符串本身结构的信息 但是我们发现由于匹配字符串第七个字符串D的(0,6)子串ABCABC匹配字符串最长公共前后缀为ABC,这意味着我们知道当前匹配字符串的后缀ABC与匹配字符串的前缀ABC相同,所以下一个字符可以直接从匹配字符串的第四个字符串进行匹配计算,我们发现第七个字符A与匹配字符串ABCABC的第四个字符A相等,因此搜索匹配到了ABCA,从第四个字符开始匹配,进而继续搜索直至搜素字符串ABCABCABCD与符合条件而找到匹配字符串的位置。

KMP算法就是依据上文描述最长公共前后缀的原理设计的。细节如下

next数组

定义

根据本文的理解next[i]的值代表当搜索字符串当前值与匹配字符串第i个字符串不相等时,退回到第next[i]个字符进行比较。

计算过程

next数组计算逻辑为 匹配字符串(0,i-1)的最长公共前后缀(不包含自身)的值,且next[0]=-1

理解重点是next[i]与当前第i字符无关,而与当前字符分隔的前缀子串(0,i-1)有关,值为(0,i-1)最长公共前后缀长度的值,最长公共前后缀代表了后缀等于前缀,因此可以从前缀的下一个字符开始搜索,而下一个字符刚好就是第next[i]个字符,这里十分的巧妙,理解了这个逻辑之后,查看下文求计算next数组java代码,next数组就变为了求当前第i个字符分隔的子串(0,i)的最长公共前后缀,for循环中利用相关信息即可求解(如果这里需要解释的话请留言,本文会改文章专门说明),类似dp算法。k = next[k] 也就是回退到已经算过的最长公共前后缀的位置重新开始计算,本质上是一个动态规划过程。能理解这个说明你已经理解了KMP算法核心。kmp算法中next右移什么的奇怪操作就可以忘记了。

    public static int[] getNext(String ps) {
        char[] p = ps.toCharArray();
        int[] next = new int[p.length];
        next[0] = -1;  //第一个字符串 不存在前后缀
        int j = 0;  //next[j]为计算过程,当遇到(k == -1 || p[j] == p[k])时计算一位
        int k = -1; //next数组的值
        while (j < p.length - 1) {  //因为next[0]=-1,所以j < p.length - 1只需要计算length-1次
            if (k == -1 || p[j] == p[k]) {
                next[++j] = ++k;  // 当k=-1时说明,刚开始匹配
            } else {
                k = next[k];
            }
        }
        return next;
    }

字符串匹配过程

参考KMP字符串匹配代码 t为搜索字符串,p为匹配字符串

当j==-1时说明当前搜索字符串的字符与匹配字符串第一个字符都不相等,需要右移到下一个字符进行重新开始。

当t[i]==p[j]时表明当前搜索字符串第i个字符与匹配字符串第[j]个字符相同,匹配下一个字符

当不为以上两种情况时,说明搜索字符串第i个字符与匹配字符串第j(j>=0)个字符不相等,所以根据求得的next数组,回退到匹配字符串next[j]个字符进行比较。

重复以上过程当j=p.length时,匹配成功,字符串匹配问题存在解。其他的变形,如找出所有匹配的字符串位置,可根据需要进行代码改在即可。

    public static int KMP(String ts, String ps) {

        char[] t = ts.toCharArray();
        char[] p = ps.toCharArray();

        int i = 0; // 主串的位置
        int j = 0; // 模式串的位置
        int[] next = getNext(ps);
        while (i < t.length && j < p.length) {
            if (j == -1 || t[i] == p[j]) { // j==-1 当前搜索位置与匹配字符串无公共前缀,且与第一个字符不相等,从下一个为位置止重新开始匹配
                i++;
                j++;
            } else {
                // i不需要回溯了
                // i = i - j + 1;
                j = next[j]; // j退回到指定位置
            }

        }
        if (j == p.length) {
            return i - j;
        } else {
            return -1;
        }
    }

完整代码

public class KMPAlgorithm {

    public static int[] getNext(String ps) {
        char[] p = ps.toCharArray();
        int[] next = new int[p.length];
        next[0] = -1;
        int j = 0;
        int k = -1;
        while (j < p.length - 1) {
            if (k == -1 || p[j] == p[k]) {
                next[++j] = ++k;
            } else {
                k = next[k];
            }
        }
        return next;
    }

    public static int KMP(String ts, String ps) {

        char[] t = ts.toCharArray();

        char[] p = ps.toCharArray();

        int i = 0; // 主串的位置
        int j = 0; // 模式串的位置
        int[] next = getNext(ps);
        while (i < t.length && j < p.length) {
            if (j == -1 || t[i] == p[j]) { //   当前搜索位置与匹配字符串不可能无关联,到下一个为止重新开始匹配
                i++;
                j++;
            } else {
                j = next[j]; // j退回到指定位置
            }
        }
        if (j == p.length) {
            return i - j;  //i-j为搜索字符串中匹配字符串开始位置
        } else {
            return -1; // -1 代表搜索不到
        }
    }

    public static void main(String[] args) {
        String text = "ababcabcabababd";
        String pattern = "ababd";
        int result = KMP(text, pattern);
        if (result != -1) {
            System.out.println("模式串在文本中的起始位置: " + result);
        } else {
            System.out.println("未找到模式串");
        }
    }

}

如果对你有用的话,请一键三连,感谢支持