算法系列-KMP算法和JAVA实现

Knuth-Morris-Pratt 字符串查找算法 ,简称 KMP 算法:常用与在一个文本字符串 s 内查找一个模>式串 P 的出现位置

该算法由 Donald Knuth、Vaughan Pratt、James H. Morris 三人于 1977 年联合发表,故取这 3 人的>姓氏命名此算法.

基本介绍

KMP(Knuth-Morris-Pratt)算法是一种高效的字符串匹配算法,用于在一个文本字符串中查找一个模式字符串的出现位置。它的主要优势在于在匹配过程中避免了不必要的回溯操作,从而提高了算法的效率。

KMP算法的核心思想是利用模式字符串的自身特性,构建一个部分匹配表(Partial Match Table),也称为前缀函数(prefix function)表。这个表存储了模式字符串中每个位置上的最长相等的真前缀和真后缀的长度。

算法的匹配过程中,通过根据部分匹配表进行移动,避免在模式字符串和文本字符串中进行不必要的回溯。具体步骤如下:

  1. 预处理模式字符串,构建部分匹配表。
  2. 在文本字符串中从左到右逐个字符进行匹配。
  3. 当发现不匹配的字符时,利用部分匹配表确定模式字符串的移动位置,从而继续匹配。

通过利用部分匹配表,KMP算法避免了在模式字符串中进行不必要的回溯,从而达到了线性时间复杂度的匹配效果,即O(m+n),其中m为模式字符串的长度,n为文本字符串的长度。

KMP算法在字符串匹配问题中得到广泛应用,特别是在处理大文本和长模式字符串时,相对于暴力匹配算法具有显著的性能优势。

思路分析

以下面字符串为例

java 复制代码
Str1 = "BBC ABCDAB ABCDABCDABDE"
Str2 = "ABCDABD"
  1. 都用第 1 个字符进行比较,不符合,关键词(文本串)向后移动一位

  2. 重复第一步,还是不符合,再后移动

  3. 一直重复,直到 str1 有一个字符与 str2 的第一个字符匹配为止

  4. 接着比较字符串和搜索词的下一个字符,还是符合

  5. 遇到 st1 有一个字符与 str2 对应的字符不符合时

  6. 这时候:想到的是继续遍历 st1 的下一个字符(也就是暴力匹配)

    这时,就出现一个问题:

    此时回溯时,A 还会去和 BCD 进行比较,而在上一步 ABCDAB 与 ABCDABD,前 6 个都相等,其中 BCD 搜索词的第一个字符 A 不相等,那么这个时候还要用 A 去匹配 BCD,这肯定会匹配失败。

    KMP 算法的想法是:设法利用这个已知信息,不要把「搜索位置」移回已经比较过的位置,继续把它向后移,这样就提高了效率。

    那么新的问题就来了:你如何知道 A 与 BCD 不相同,并且只有 BCD 不用比较呢?这个就是 KMP 的核心原理了。

  7. KMP 利用 部分匹配表,来省略掉刚刚重复的步骤。

    上表是这样看的:

    1. ABCD 匹配值 0
    2. ABCDA 匹配值 1
    3. ABCDAB 匹配值 2

    至于如何产生的这个部分匹配表,下面专门讲解,这里你要知道的是,KMP 利用这个 部分匹配表 可以省略掉重复的步骤

  8. 已知空格与 D 不匹配时,前面 6 个字符 ABCDAB 是匹配的。

    查表可知:部分匹配值是 2,因此按照下面的公司计算出后移的位数:

    ini 复制代码
    移动位数 = 已匹配的字符数 - 对应的部分匹配值
    4      =    6		  -   2

    因此回溯的时候往后移动 4 位,而不是暴力匹配移动的 1 位。

  9. 因为空格与 C 不匹配

    搜索词还要继续往后移动,这时,已匹配的字符树数为 2 (AB),对应的 部分匹配值 为 0,所以 移动位数 = 2 - 0 = 2,于是将搜索词(文本串)向后移动两位

  10. 因为空格与 A 不匹配,继续往后移动一位

  11. 此时:部分匹配表已用完,需要一位一位进行匹配

直到发现 C 与 D 不匹配。于是,移动位数 = 6-2,继续将搜索词(文本串)往后移动 4 位。 12. 1. 逐位比较,直到搜索词(文本串)的最后一位,发现完全匹配,搜索完成。

less 复制代码
![image-20210103221650411](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/860f3ee84489443e9808022e8bacc0f5~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=667&h=138&s=33434&e=png&b=fdfdfd)

如果还要继续搜索(即找出完全匹配),`移动位数 = 7 - 0`,再将搜索词向后移动 7 位,这里就不再重复了。

部分匹配表

看上上述步骤,你现在的疑惑是:这个部分匹配表是如何产生的?下面就来介绍

需要先知道 **前缀 ** 和 后缀 是什么

  • 前缀:仔细看,它的前缀就是每个字符串的组合,逐渐变长,但是不包括最后一个字符

    如果 bread 是字符串 bread 的前缀,这个不是完全匹配了吗?

  • 后缀:同理,不包含第一个

部分匹配值 就是 前缀后缀最长的共有元素的长度 ,下面以 ABCDABD 来解说:

字符串 前缀 后缀 共有元素 共有元素长度
A - - - 0
AB A B - 0
ABC A、AB BC、C - 0
ABCD A、AB、ABC BCD、CD、D - 0
ABCDA A、AB、ABC、ABCD BCDA、CDA、DA、A A 1
ABCDAB A、AB、ABC、ABCD、ABCDA BCDAB、CDAB、DAB、AB、B AB 2
ABCDABD A、AB、ABC、ABCD、ABCDA、ABCDAB BCDABD、CDABD、DABD、ABD、BD、D - 0

部分匹配 的实质是:有时候,字符串头部和尾部会有重复。

比如:ABCDAB 中有两个 AB ,那么它的 部分匹配值 就是 2 (AB 的长度),搜索词(文本串)移动的时候,第一个移动 4 位(字符串长度 - 部分匹配值),就可以来到第二个 AB 的位置,从而跳过了已经匹配过的 BCD。

如果还是想刨根问底,可以去参考下这篇文章:写得很详细很详尽KMP算法,应该需要一些数学知识才能看懂。

代码实现

主要的就有两步:

  1. 得到子串的部分匹配表
  2. 使用部分匹配表完成 KMP 匹配

JAVA代码实现

java 复制代码
/**
 * kmp 搜索: 推导过程
 */
@Test
public void kmpSearchTest1() {
    String str1 = "BBC ABCDAB ABCDABCDABDE";
    String str2 = "BBC";
    // 获得部分匹配表
    int[] next = buildKmpNext(str2);
    int result = kmpSearch(str1, str2, next);
    System.out.println(result);

    str2 = "ABCDABD";
    // 获得部分匹配表
    next = buildKmpNext(str2);
    result = kmpSearch(str1, str2, next);
    System.out.println(result);
}

/**
 * 生成此字符串的 部分匹配表
 *
 * @param dest
 */
public int[] buildKmpNext(String dest) {
    int[] next = new int[dest.length()];
    // 第一个字符的前缀和后缀都没有,所以不会有公共元素,因此必定为 0
    next[0] = 0;
    for (int i = 1, j = 0; i < dest.length(); i++) {
        /*
          ABCDA
          前缀:`A、AB、ABC、ABCD`
          后缀:`BCDA、CDA、DA、A`
          公共元素 A
          部分匹配值:1
         */
        // 当  dest.charAt(i) != dest.charAt(j) 时
        // 需要从 next[j-1] 中获取新的 j
        // 这步骤是 部分匹配表的 核心点
        while (j > 0 && dest.charAt(i) != dest.charAt(j)) {
            j = next[j - 1];
        }
        // 当相等时,表示有一个部分匹配值
        if (dest.charAt(i) == dest.charAt(j)) {
            j++;
        }
        next[i] = j;
    }
    return next;
}

/**
 * kmp 搜索算法
 *
 * @param str1 源字符串
 * @param str2 子串
 * @param next 子串的部分匹配表
 * @return
 */
private int kmpSearch(String str1, String str2, int[] next) {
    for (int i = 0, j = 0; i < str1.length(); i++) {
        while (j > 0 && str1.charAt(i) != str2.charAt(j)) {
          /*
          从源字符串中挨个的取出字符与 子串的第一个相比
          直到匹配上时:j++, 如果有一个已经匹配上了比如
                    ↓ i = 10
          BBC ABCDAB ABCDABCDABDE
              ABCDABD
                    ↑ j = 6
          然后继续下一个,这个时候由于 i 与 j 同步在增加,直到匹配到 空格与 D 时,不匹配
          此时:需要调整子串的匹配其实点:
          j = next[6 - 1] = 2, 调整后

                    ↓ i = 10
          BBC ABCDAB ABCDABCDABDE
                  ABCDABD
                    ↑ j = 2

          会发现不等于,继续调整
          j = next[2 - 1] = 0, 调整后

                    ↓ i = 10
          BBC ABCDAB ABCDABCDABDE
                     ABCDABD
                     ↑ j = 0
          此时:不满足 j > 0 了
         */
            j = next[j - 1];
        }

        /*
          从源字符串中挨个的取出字符与 子串的第一个相比
          直到匹配上时:j++, 如果有一个已经匹配上了比如
              ↓ i = 4
          BBC ABCDAB ABCDABCDABDE
              ABCDABD
              ↑ j = 0
          然后继续下一个,这个时候由于 i 与 j 同步在增加,直到匹配到 空格与 D 时,不匹配
         */
        if (str1.charAt(i) == str2.charAt(j)) {
            j++;
        }
        if (j == str2.length()) {
            return i - j + 1;
        }
    }
    return -1;
}

运行结果

java 复制代码
0
15

参考

1.KMP 算法

2.字符串匹配的KMP算法

相关推荐
huapiaoy4 分钟前
Redis中数据类型的使用(hash和list)
redis·算法·哈希算法
冷白白17 分钟前
【C++】C++对象初探及友元
c语言·开发语言·c++·算法
鹤上听雷25 分钟前
【AGC005D】~K Perm Counting(计数抽象成图)
算法
睡觉然后上课35 分钟前
c基础面试题
c语言·开发语言·c++·面试
一叶祇秋37 分钟前
Leetcode - 周赛417
算法·leetcode·职场和发展
武昌库里写JAVA42 分钟前
【Java】Java面试题笔试
c语言·开发语言·数据结构·算法·二维数组
ya888g43 分钟前
GESP C++四级样题卷
java·c++·算法
Funny_AI_LAB1 小时前
MetaAI最新开源Llama3.2亮点及使用指南
算法·计算机视觉·语言模型·llama·facebook
NuyoahC1 小时前
算法笔记(十一)——优先级队列(堆)
c++·笔记·算法·优先级队列
jk_1011 小时前
MATLAB中decomposition函数用法
开发语言·算法·matlab