KMP算法

章节目录:

    • 一、暴力匹配算法
      • 1.1 算法介绍
      • 1.2 算法步骤
      • 1.3 应用案例
    • 二、KMP匹配算法
      • 2.1 算法介绍
      • 2.2 算法步骤
      • 2.3 部分匹配表
      • 2.4 应用案例
    • 三、结束语

一、暴力匹配算法

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)。

  • 匹配过程-示意图

KMP算法_第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)
  • 示意图

KMP算法_第2张图片

KMP算法_第3张图片

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;
    }
}

三、结束语


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

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

你可能感兴趣的:(数据结构与算法,算法,java,开发语言)