KMP 算法理论及实现 (Java)

KMP 算法理论及实现 (Java)

一、简介

KMP 算法是一种字符串匹配算法。核心思想是利用匹配失败之后的信息,尽量减少模式串与主串的匹配次数来达到快速匹配的目的。理应一个 next() 函数实现,该函数本身包含了模式串的局部匹配信息。KMP 算法的时间复杂度是 O(m+n)

二、问题引入

有一个目标串 S ,和一个模式串 P,现在要寻找模式串 P 是否在目标串 S 中以及出现的位置

三、暴力算法

不做介绍

四、KMP 算法

1. 算法思想介绍

  • 以下图所示主串及模式串为例

  • 依次向下对比,直到出现不匹配的情况

  • 通过观察可知模式串在已经匹配的字符串内有公共前后缀,取最长公共前后缀

  • 而后,直接移动模式串,使得前缀移动到后缀的位置

    • 注:这样使前缀直接移动到后缀的方式是成立的,可以证明中间没有匹配的情况 (后文有证明)
  • 再次对比,发现不匹配。这里由于选取的前后缀内容是相同的,所以当前缀到达后缀的位置后,只需要对指针当前位置及以后进行对比即可,指针前面的前缀不需要再次对比

  • 再次寻找最长公共前后缀

  • 移动模式串

  • 再次对比,发现匹配成功

2. 模式串移动证明

  • 以下图所示主串及模式串为例

    • 上图中,模式串的最长前后缀为 A,并且匹配到箭头所指位置发现不匹配,即箭头之前是完全匹配的
  • 现假设在移动前缀到后缀位置的过程中遗漏了一种匹配的情况,如下图所示 模式串2(模式串2 和 模式串 相同)

  • 因为 模式串2 在此处与主串相匹配,并且假设 模式串2 的第一个 A 与主串的第二个 A 之间的部分称为 B 部分,由于 模式串2 与主串匹配,故可以确定 模式串2 的一部分内容

  • 又因为模式串在箭头所指之前与主串完全匹配,所以可以推出模式串部分内容

  • 又因为 模式串2 和 模式串相同,所以,更新模式串的内容如下

  • 显然,最长公共前后缀变成了 ABA ,与开始的假设 最长公共前后缀为 A 矛盾。
  • 所以 将模式串最长前缀移动到最长后缀位置时中间可能会有遗漏的匹配的情况 命题不成立

3. 确定模式串移动距离

  • 由前文 “算法思想介绍” 已知,模式串匹配时向后移动的距离只取决于模式串自己,所以可以对模式串单独分析,建立一个 next[] 数组记录在模式串对应的元素不匹配时向后移动多少距离
  • 以下图所示模式串为例

  • 建立对应的 next[] 数组

    • 如图,next[i] 表示在 0 ~ i 这个字串的最长公共前后缀的长度。如,next[3] 表示 ABAA 这个字串的公共前后缀的长度,显然是 1
  • next[] 数组的含义
    • 当 模式串[i] 与主串发生不匹配时,就将 模式串[next[i-1]] 移动到当前位,再进行比较。比如,如果 模式串[5] 与主串发生不匹配,由于 next[4] = 2,所以将 模式串[2],即 A 拉到当前位再与主串相比较
    • 也就是说,如果失配在 模式串[i],那么 模式串[i] ~ 模式串[i-1] 这一段里,前 next[i-1] 个字符恰好和后 next[i-1] 个字符相等,也就是说可以拿长度为 next[i-1] 的那一段前缀来顶替当前字串的后缀的位置,让匹配继续

4. 快速构建 next[ ] 数组

快速构建  next[] 数组的核心思想为: 模式串自己与自己做匹配。即, 使用递推的方式求解 next[] 数组
  • 情况一:当前后缀的下一位与当前前缀的下一位相同

    • 以上图所示模式串为例,已知 next[3] = 1,即最长公共前后缀长度为 1 ,即 A,此时要求 next[4],由于 0 ~ 3 这一字串的最长后缀向后加一为 AB,最长前缀向后加一也为 AB,所以 next[4] = next[3] + 1 = 2

  • 情况二:当前后缀的下一位与当前前缀的下一位不同
    • 以下图所示模式串为例

    • 图中,next[12] = 5,字串的最长公共前后缀为 ABCAB,但是,在使用递推的方式求 next[13] 的时候发现,最长后缀加一位和最长前缀加一位之后并不相同
      • 记 模式串[0] ~ 模式串[12]的公共前后缀为 L0,并且作为前缀的部分记为L0+,作为后缀的部分记为 L0-模式串[0] ~ 模式串[13]的公共前后缀为 L1
      • 此时,L0 向后扩大的方向走不通,但是我们的目的是通过 L0 求得 L1
      • 已知此时 L0 扩大时不匹配,所以 L1 一定要比 L0 短。即,只看前缀的话,L1 一定落在 L0+ 内。
      • 由于我们的目的依然是 通过 L0 求得 L1,所以我们可以在当前 L0 的范围内,缩短 L0 的长度,再通过向后扩大一位 L0 而后进行比较的方式来求得 L1 。
      • 但是,如何得知 L0 缩小的长度呢
      • 观察 L0 ,已知 L0+ 和 L0- 相等,但是 L0+ 和 L0- 也存在自己的前后缀(尽管它们是对应相同的),当 L0+ 存在 前缀 和 L0- 的 后缀 相同时(即 L0 有公共的前后缀),此时,如上图所示,由于 L0 有公共前后缀 L00+ L00-,所以将指针 P0 移动到 2 的位置,即 C,再进行比较( i = next[i-1] )

      • 因为目的是 通过 L0 求得 L1,L1+ 和 L1- 是相同的,所以在一个值匹配不上的时候,先找到这个值前面已经被匹配的部分,即 L0 的公共前后缀,这样的部分可能有多个,在找到这个值前面已经被匹配到的部分之后,再进行对这个值的匹配,如果还是不成功,就重复以上步骤,如果所有的公共前后缀都是用完,仍然没有成功,那么这个值对应的 next[ ] 数组置零

5. 算法实现(Java)

package KMP算法;/*
 *author:yangyu
 *creation time:2023/6/8 23:32
 */

public class Kmp {
    public static void getNext(int[] next, String sub) {
        next[0] = -1;
        if (sub.length() == 1) {
            // 当子串只有一个数据的时候,next数组的长度为1
            return;
        }
        // 前提条件是数组长度大于1
        next[1] = 0;
        int k = 0;
        int j = 2;

        while (j < sub.length()) {
            if (k == -1 || sub.charAt(j - 1) == sub.charAt(k)) {
                next[j] = k + 1;
                j++;
                k++;
            } else {
                k = next[k];
            }
        }
    }

    public static int KMP(String str, String sub, int pos) {
        // 判断两个串不能为空
        if (str == null || sub == null) {
            return -1;
        }

        int i = pos;// i遍历主串  从pos位置开始
        int j = 0;  // j遍历字串  从0开始
        int strLength = str.length();
        int subLength = sub.length();

        if (strLength == 0 || subLength == 0) {
            return -1;
        }
        // 判断pos位置合法性
        if (pos < 0 || pos > strLength) {
            return -1;
        }

        //求字串的next数组
        int[] next = new int[subLength];
        getNext(next, sub);

        while (i < strLength && j < subLength) {
            if (j == -1 || str.charAt(i) == sub.charAt(j)) {
                i++;
                j++;
            } else {
                j = next[j];
            }

        }
        if (j == subLength) {
            // 字串遍历完之后 j应该等于sublength
            // 找到返回字串在主串中的起始位置
            return i - j;
        } else {
            // 找不到返回-1
            return -1;
        }

    }

    public static void main(String[] args) {
        String str = "ababcabcdabcdefg";
        String sub = "aabaabb";

        int pos = KMP(str, sub, 0);
        System.out.println(pos);
    }

}
  • 24
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

.鱼子酱

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值