KMP 算法字符串匹配 vs. String 类 indexOf() 暴力匹配

KMP算法

介绍:KMP 算法是一种改进的字符串匹配算法,名称由三个发明者姓名的首字母组成。算法的核心是利用匹配失败后的信息,尽量减少模式串 P 与主串 S 的匹配次数以达到快速匹配的目的。具体实现就是通过一个 next[] 数组实现,数组本身包含了模式串的局部匹配信息(算法思想参考b站视频)。KMP 算法的时间复杂度为 O(m+n),其中m、n分别是模式串、主串的长度。

1. 什么是 next[] 数组?

前缀/后缀:指除了自身以外,一个字符串的全部头部/尾部组合。例如字符串 china,前缀包括 c、ch、chi、chin,后缀包括 hina、ina、na、a。

最长的相同前后缀:例如 abcjkdabc 最长的相同前后缀是 abc(长度3);cbcbc 最长的相同前后缀是cbc(长度3);abcbc 最长的相同前后缀是不存在的(长度0)。

next[] 数组:表示在模式串 P 中,当前下标对应的字符之前的字符串中,最长的相同前后缀。例如 next [i] = j,表示在模式串 P 中, [0, i - 1] 区段中有最长的相同前后缀为 j。

2. 如何计算 next[] 数组?

规定next[0] = -1,假设已知 next[i] = j,如何求出next[i+1]

  • 情况一:如果 p[i] = p[j],则 next[i+1] = next[i] + 1。

    图中红色部分内容相等,表示字符串 [0, i - 1] 区段中最长的相同前后缀为 j。当 p[i] = p[j] 时,红色部分扩展为绿色部分。
    在这里插入图片描述

  • 情况二:如果 p[i] != p[j],则令 j = next[j],然后继续判断 p[i] = p[j]。

    当 p[i] != p[j] 时,需要递归前缀索引,next[j] 表示 [0, j - 1] 区段中最长的相同前后缀,即图中第一和第二个浅绿色椭圆内容相同。因为红色部分内容相等,所以四个浅绿色椭圆内容相等。令 j = next[j],即利用第一和第四个浅绿色椭圆内容相同来加快得到 [0, i - 1] 区段的相同前后缀的长度。

    也可以这样理解:前面 [0, i - 1] 区段匹配主串成功,此时在 i 处失败,由于第一和第四个浅绿色椭圆内容相同,那么下次可以直接从 next[j] 处开始匹配。这也是 next[] 数组的另一种含义。
    在这里插入图片描述

3. 为何要改进 next[]数组?

i0123456
模式串ABCDABD
next[ i ]-1000012

观察 next[5] = 1,表示当 B 匹配失败时,下次从下标 1 开始匹配,跳过之前的字符匹配。但实际上,下标为 1 的字符仍然还是 B,显然下次匹配一定会失败,于是修改 next[5] = next[ next[5] ] = 0,同理 next[4] = -1。

4. 为何模式串 P 移动的过程中不可能存在匹配?

反证法:如下图所示,M为主串,N为模式串,假设 e 是当前最长的相同前后缀。当蓝色部分匹配失败时,表明 M[y - j, y] = N[0, j],即M、N两竖线间的内容完全相同,下一步应该作出绿色部分的移动。

如果此时存在 f 使得模式串 N 匹配成功,则表明 ① = ②。因为M、N两竖线间的内容完全相同,所以 ② = ③;又因为①是由④移动得到,所以① = ④。从而③ = ④,即 f 也是相同前后缀,显然 f > e,与假设矛盾,所以模式串移动过程中不可能存在匹配。
在这里插入图片描述

参考:KMP思想(b站视频)KMP代码实现next数组理解KMP证明

public class KMP {
    // KMP算法匹配字符串,匹配成功返回P在S中的首字符下标,匹配失败返回-1
    public static int indexOf(String source, String pattern) {
        char[] src = source.toCharArray();
        char[] ptn = pattern.toCharArray();
        int sLen = src.length;
        int pLen = ptn.length;

        // 模式串为空字符串,返回0
        if (pLen == 0) {
            return 0;
        }
        // 主串长度小于模式串长度,返回-1
        if (sLen < pLen) {
            return -1;
        }

        int[] next = getNext(ptn);
        int i = 0;  // 主串S的下标
        int j = 0;  // 模式串P的下标
        while (i < sLen && j < pLen) {
            if (j == -1 || src[i] == ptn[j]) {
                i++;
                j++;
            } else {
                // 匹配失败时使用next[]数组加快匹配
                j = next[j];
            }
        }

        if (j == pLen)
            return i - j;
        return -1;
    }

    // 获取模式串P对应的next[]数组
    private static int[] getNext(char[] p) {
        int pLen = p.length;
        int[] next = new int[pLen];
        int i = 0;  // 后缀下标
        int j = -1; // 前缀下标
        next[0] = -1; // 初始next[0]为-1

        while (i < pLen - 1) {
            if (j == -1 || p[i] == p[j]) {
                i++;
                j++;
                // 未优化的next[]数组求法
                // next[i] = j;
                // 优化的next[]数组求法
                if (p[i] != p[j]) {
                    next[i] = j;
                } else {
                    next[i] = next[j];
                }
            } else {
                j = next[j];
            }
        }
        return next;
    }
}

补充:String 类 indexOf() 字符串匹配源码

public class SourceCode {
    // 算法思想其实就是暴力匹配,使用双指针依次进行比较
    public static int indexOf(String source, String pattern) {
        char[] src = source.toCharArray();
        char[] ptn = pattern.toCharArray();
        int sLen = src.length;
        int pLen = ptn.length;

        if (pLen == 0) {
            return 0;
        }
        if (sLen == 0) {
            return -1;
        }

        char first = ptn[0];
        int max = sLen - pLen;
        for (int i = 0; i <= max; i++) {
            // 首先查找第一个字符
            if (src[i] != first) {
                while (++i <= max && src[i] != first) ;
            }
            // 找到第一个字符,现在匹配剩余字符
            if (i <= max) {
                int j = i + 1;	// 主串和模式串都从下一位开始
                int end = j + pLen - 1;
                for (int k = 1; j < end && src[j] == ptn[k]; j++, k++) ;
                if (j == end) {
                    // 匹配整个字符串
                    return i;
                }
            }
        }
        return -1;
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值