KMP算法

文章介绍了两种字符串匹配算法:暴力匹配算法及其工作原理,包括其时间复杂度和应用案例;接着详细阐述了KMP算法,重点讨论了部分匹配表的概念以及如何利用部分匹配表提高匹配效率,最后给出了KMP算法的实现示例。
摘要由CSDN通过智能技术生成

一、暴力匹配算法

1.1 算法介绍

字符串暴力匹配算法Brute Force Algorithm),又称为朴素的字符串匹配算法(Naive String Matching),在计算机科学中指的是一种在较长的文本串中查找子串的方法。

  • 工作原理:它顺序地将子串与文本中的每一个字符进行匹配,直到找到完全匹配的子串或遍历完整个文本。

  • 优点:易于实现和理解。

  • 缺点:匹配效率不高,特别是在长文本和子串的情况下(会有大量的回溯,每次只移动一位,若是不匹配,移动到下一位接着判断,浪费了大量的时间)。

  • 时间复杂度:此算法的时间复杂度为 O(nm) ,但是在一些实际应用场景中,可以通过优化匹配规则,降低运行时间。

  • 补充说明:现代计算机科学中已有更高效的字符串匹配算法,如 Knuth-Morris-Pratt 算法( KMP算法 )和 Boyer-Moore 算法等。

1.2 算法步骤

  1. 首先,设定两个字符串:一个较长的文本串S(长度为n),一个要匹配的子串P(长度为m);
  2. 指针 i 遍历文本串S,从0开始,设定指针 j 指向子串P的第一个字符;
  3. 比较S的第 i 个字符和T的第 j 个字符,如果它们相等,则将 i 和 j 同时加1,重复此步骤。如果不相等,将 i 回溯到上次匹配的下一位置,并将 j 重置为0
  4. j 等于子串P的长度m时找到了完全匹配的子串,即第 i-m 个字符开始;
  5. 如果 i 遍历到文本串末尾,则匹配结束

1.3 应用案例

假设有一个文本字符串"BBC ABCDAB ABCDABCDABDE",和一个模式子串"ABCDABD",现在需要查找模式子串在文本字符串中的位置(若存在该模式子串,就返回第一次出现的位置;若没有,则返回-1)。

  • 匹配过程-示意图

在这里插入图片描述

  • 代码示例
public class ViolenceMatchDemo {

    public static void main(String[] args) {
        // 测试一:存在的模式子串。
        System.out.println(violenceMatch("BBC ABCDAB ABCDABCDABDE", "ABCDABD"));
        // 15

        // 测试二:不存在的模式子串。
        System.out.println(violenceMatch("BBC ABCDAB ABCDABCDABDE", "X"));
        // -1
    }

    /**
     * 字符串暴力匹配。
     *
     * @param text    文本串
     * @param pattern 模式子串
     * @return int    找到则返回第一次出现的索引,否则返回 -1
     */
    public static int violenceMatch(String text, String pattern) {

        int tLen = text.length();
        int pLen = pattern.length();

        // 辅助指针:i指向文本串;j指向模式子串。
        int i = 0;
        int j = 0;

        // 避免扫描越界。
        while (i < tLen && j < pLen) {
            // 匹配成功,指针同时顺序后移。
            if (text.charAt(i) == pattern.charAt(j)) {
                i++;
                j++;
                // 匹配失败,i回溯到上次匹配的下一位置,j重置为0。
            } else {
                i = i - (j - 1);
                j = 0;
            }
        }

        // 判断是否找到了完全匹配的模式子串。
        if (j == pLen) {
            return i - j;
        } else {
            return -1;
        }
    }
}

二、KMP匹配算法

那有没有一种算法,让指针i不往回退,只需要移动指针j即可呢?

2.1 算法介绍

Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”,常用于在一个文本串S内查找一个模式串P 的出现位置,这个算法由Donald KnuthJames H. MorrisVaughan Pratt 三人于1977年联合发表,故取这3人的姓氏命名此算法。

  • KMP算法核心利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的(具体实现就是通过一个 next() 函数实现,函数本身包含了模式串的局部匹配信息)。
  • 时间复杂度:如果文本串的长度为n,模式串的长度为m,那么匹配过程的时间复杂度为O(n),算上计算next的O(m)时间,KMP的整体时间复杂度为O(m + n)

2.2 算法步骤

  • 假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置:

    • 如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),则令指针同时加1,继续匹配下一个字符;
    • 如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]。此举意味着失配时,模式串P相对于文本串S向右移动了j - next [j] 位。(换言之,当匹配失败时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值,即移动的实际位数为:j - next[j],且此值大于等于1)
  • 示意图

在这里插入图片描述

在这里插入图片描述

2.3 部分匹配表

  • “部分匹配值”就是”前缀”和”后缀”的最长的共有元素的长度。以”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”的长度)。搜索词移动的时候,第一个”AB”向后移动 4 位(字符串长度- 部分匹配值),就可以来到第二个”AB”的位置。

2.4 应用案例

需求:通过KMP算法完成文本字符串"BBC ABCDAB ABCDABCDABDE",和一个模式子串"ABCDABD"的匹配。

  • 代码示例
public class KMPAlgorithmDemo {

    public static void main(String[] args) {
        // 文本串。
        String text = "BBC ABCDAB ABCDABCDABDE";
        // 模式子串。
        String pattern = "ABCDABD";

        // 模式子串的部分匹配值表。
        int[] next = kmpNext(pattern);
        System.out.println("next=" + Arrays.toString(next));
        // next=[0, 0, 0, 0, 1, 2, 0]

        int index = kmpSearch(text, pattern, next);
        System.out.println("index=" + index);
        // index=15
    }

    /**
     * kmp(改良暴力匹配算法)。
     *
     * @param text    文本串
     * @param pattern 模式子串
     * @param next    模式子串对应的部分匹配表
     * @return int    找到则返回第一次出现的索引,否则返回 -1
     */
    public static int kmpSearch(String text, String pattern, int[] next) {

        int tLen = text.length();
        int pLen = pattern.length();

        for (int i = 0, j = 0; i < tLen; i++) {
            // 字符匹配失败时,调整指针j的位置。
            while (j > 0 && text.charAt(i) != pattern.charAt(j)) {
                j = next[j - 1];
            }

            // 字符匹配成功,指针j后移一位。
            if (text.charAt(i) == pattern.charAt(j)) {
                j++;
            }

            // 找到了。
            if (j == pLen) {
                return i - j + 1;
            }
        }
        return -1;
    }

    /**
     * 获取到一个模式子串的部分匹配值表。
     *
     * @param pattern 模式子串
     * @return {@link int[]} 对应的部分匹配值
     */
    public static int[] kmpNext(String pattern) {
        // 部分匹配表。
        int[] next = new int[pattern.length()];
        // 如果字符串是长度为1,部分匹配值就是0。
        next[0] = 0;

        for (int i = 1, j = 0; i < pattern.length(); i++) {
            // 从next[j-1]获取新的j。
            while (j > 0 && pattern.charAt(i) != pattern.charAt(j)) {
                j = next[j - 1];
            }

            // 满足时,部分匹配值就是+1。
            if (pattern.charAt(i) == pattern.charAt(j)) {
                j++;
            }
            next[i] = j;
        }
        return next;
    }
}

三、结束语


“-------怕什么真理无穷,进一寸有一寸的欢喜。”

微信公众号搜索:饺子泡牛奶

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值