串的模式匹配算法-暴力、KMP算法详解

本文深入讲解KMP算法的原理及应用,包括模式匹配算法的基本思想、KMP算法的概念及其优化之处,同时给出了详细的代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

串的模式匹配算法-KMP算法

简介

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。

简单的模式匹配算法(暴力匹配算法)

假设现在我们面临这样一个问题:有一个文本串S,和一个模式串P,现在要查找P在S中的位置,如何查找呢?

思路

如果用暴力匹配的思路,并假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置,则有:

  • 如果当前字符匹配成功(即S[i] == P[j]),则i++,j++,继续匹配下一个字符;
  • 如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0。

时间复杂度

算法时间复杂度分析:

1.最好的情况,即不需要回溯,一次匹配成功,复杂度为O(m)
2.最坏的情况,需要多次回溯时,复杂度为O(n×m)
3.算法平均时间复杂度为O(n×m)

代码实现

下面给出暴力匹配算法的代码:

/**
     * 暴力匹配算法(又称简单的模式匹配算法)
     * Brute Force: O(mn)
     * @param s 目标串
     * @param p 模式串
     * @return 如果匹配成功,返回下标,否则返回-1
     */
    public static int stringMatch(String s, String p) {
        if (s.equals(p)) {
            return 0;
        }
        if (s.length() < p.length()) {
            return -1;
        }

        int i = 0, j = 0;
        while (i < s.length() && j < p.length()) {
            // 如果当前字符匹配成功(即S[i] == P[j]),则i++,j++,继续匹配下一个字符
            if (s.charAt(i) == p.charAt(j)) {
                i++;
                j++;
            } else {
                // 如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0
                i = i - j + 1;
                j = 0;
            }
        }
        if (j == p.length()) {
            return i - j;
        } else {
            return -1;
        }
    }

每次失败都要回溯匹配,导致局部匹配重复。

改进的模式匹配算法——KMP算法

概念

  • 前缀:除最后一个字符以外,字符串所有的头部子串
  • 后缀:出第一个字符外,字符串的所有尾部子串
  • 最长相等前后缀(又称部分匹配值或最长公共前后缀):字符串前缀和后缀的最长相等前后缀

如下所示,找出最长相同前后缀:

在这里插入图片描述

next数组

next 数组相当于“最大长度值” 整体向右移动一位,然后初始值赋为-1,即
在这里插入图片描述

思路图解:

​ 1.

在这里插入图片描述

首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索词"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。

2.
在这里插入图片描述

因为B与A不匹配,搜索词再往后移。

3.

在这里插入图片描述

就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。

4.

在这里插入图片描述

接着比较字符串和搜索词的下一个字符,还是相同。

5.

在这里插入图片描述

直到字符串有一个字符,与搜索词对应的字符不相同为止。

6.

在这里插入图片描述

这时,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。

7.

在这里插入图片描述

一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是"ABCDAB"。KMP算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率。

8.

在这里插入图片描述

怎么做到这一点呢?可以针对搜索词,算出一张《部分匹配表》(Partial Match Table)。这张表是如何产生的,后面再介绍,这里只要会用就可以了。

9.

在这里插入图片描述

已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此按照下面的公式算出向后移动的位数:

移动位数 = 已匹配的字符数 - 对应的部分匹配值

因为 6 - 2 等于4,所以将搜索词向后移动4位。

10.

在这里插入图片描述

因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2(“AB”),对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移2位。

11.

在这里插入图片描述

因为空格与A不匹配,继续后移一位。

12.

在这里插入图片描述

逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4位。

13.

在这里插入图片描述

逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位,这里就不再重复了。

14.

在这里插入图片描述

下面介绍《部分匹配表》是如何产生的。

首先,要了解两个概念:“前缀"和"后缀”。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。

15.

在这里插入图片描述

"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"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。

16.

在这里插入图片描述

"部分匹配"的实质是,有时候,字符串头部和尾部会有重复。比如,“ABCDAB"之中有两个"AB”,那么它的"部分匹配值"就是2("AB"的长度)。搜索词移动的时候,第一个"AB"向后移动4位(字符串长度-部分匹配值),就可以来到第二个"AB"的位置。

时间复杂度

KMP算法时间复杂度分析:

设主串s长度为n,模式串t长度为m,在KMP算法中求next数组的时间复杂度为O(m),在后面的匹配中因主串s的下标不回溯,比较次数可记为n,所以KMP算法平均时间复杂度为 O(n+m)

代码实现:

/**
 * @ClassName KMP
 * @Author Fenglin Cai
 * @Date 2021 09 22 20
 * @Description 串的KMP模式匹配算法
 **/

public class KMP {
/**
     * aabaaf 前缀表:01012 , 前缀表的数字表示的是字符从0到该下标位置的最大公共后缀数(也称最长相等前后缀长度)。
     * next表 ——》前缀表右移一位,且首位添加-1即:-101012
     * @param p
     * @return
     */
    public static int[] getNext(String p) {
        int len = p.length();
        int[] next = new int[len];
        next[0] = -1;
        int i = 0, k = -1;
        while (i < len - 1) {
            // p[k]表示前缀,p[i]表示后缀
            if (k == -1 || p.charAt(i) == p.charAt(k)) {
                ++k;
                ++i;
                next[i] = k;  //前缀的下标加一是最大相同前后缀数(最长公共前后缀数)
            } else {
                k = next[k];
            }
        }
        return next;
    }

    /**
     * 时间复杂度:O(m+n)
     * @param s 目标串
     * @param p 模式串
     * @return 如果匹配成功,返回下标,否则返回-1
     */
    private static int kmpSearch(String s, String p) {
        int sLen = s.length();
        int pLen = p.length();
        if (sLen < pLen) {
            return -1;
        }

        int[] next = getNext(p);
        // matching: O(n)
        int i = 0, j = 0;
        while (i < sLen && j < pLen) {
            //①如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++
            if (j == -1 || s.charAt(i) == p.charAt(j)) {
                i++;
                j++;
            } else {
                //②如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]
                //next[j]即为j所对应的next值
                j = next[j];
            }
        }
        if (j == pLen) {
            return i - j;
        } else {
            return -1;
        }
    }

	// 测试运行入口
    public static void main(String[] args) {
        String s = "aabaabaaf";
        String model = "aabaaf";
        int i = KMP.kmpSearch(s, model);
        System.out.println(i);
    }

}

Reference

https://blog.youkuaiyun.com/qq_45406002/article/details/115277669

https://blog.youkuaiyun.com/u010232171/article/details/41945605

https://blog.youkuaiyun.com/tracydragonlxy/article/details/106658736

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小枫学IT

如果觉得有用的话,可以支持一下

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

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

打赏作者

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

抵扣说明:

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

余额充值