Loading

40-暴力匹配 & KMP算法

应用场景:字符串匹配问题

1. 暴力匹配

《算法(第4版)》

1.1 思路分析

  • 假设现在 str1 匹配到 i 位置,子串 str2 匹配到 j 位置,则有:
    • 如果当前字符匹配成功 (即 str1[i] == str2[j]),则 i++,j++,然后继续匹配下一个字符
    • 如果匹配失败 (即 str1[i] != str2[j]),令 i = i - j + 1,j = 0;相当于每次匹配失败时,i 回溯,j 被置为 0
  • 用暴力方法解决的话就会有大量的回溯
    • 每次只移动一位;若是不匹配,则 i 便移动到 头一个相匹配的字符 的下一位 接着判断
    • 浪费了大量的时间 // 不可行!

1.2 代码实现

public class ViolenceMatch {
    public static void main(String[] args) {
        String str1 = "硅硅谷 尚硅谷你尚硅 尚硅谷你尚硅谷你尚硅你好";
        String str2 = "尚硅谷你尚硅你";
        int index = violenceMathch(str1, str2);
        System.out.println(index);
    }

    public static int violenceMathch(String str1, String str2) {
        char[] s1 = str1.toCharArray();
        char[] s2 = str2.toCharArray();
        int s1Len = s1.length;
        int s2Len = s2.length;
        int i = 0, j = 0; // i指向s1, j指向s2

        // 保证在匹配过程中, 不会出现越界
        while (i < s1Len && j < s2Len) {
            if (s1[i] == s2[j]) { // 匹配成功
                i++;
                j++;
            } else { // 匹配不上
                i = i - j + 1;
                j = 0;
            }
        }

        // 判断是否匹配成功
        if (j == s2Len) return i - j;
        else return -1;
    }
}

2. KMP算法

2.1 算法简述

  • Knuth-Morris-Pratt 字符串查找算法,简称为 "KMP算法",常用于在一个文本串 S 内查找一个模式串 P 的出现位置,这个算法由 Donald Knuth、Vaughan Pratt、James H. Morris 三人于 1977 年联合发表,故取这 3 人的姓氏命名此算法。
  • KMP 是一个解决“模式串”在“文本串”是否出现过,如果出现过,最早出现的位置的经典算法
  • KMP 算法的核心是通过一个被称为 [部分匹配表(Partial Match Table)] 的数组
    • 其中保存了“模式串”中前后最长公共子序列的长度
    • 如果“模式串”有 n 个字符,那么 PMT 就会有 n 个值
    • 每次回溯时,通过 PMT 找到,前面匹配过的位置,省去了大量的计算时间

2.2 思路分析

a. 通过案例整理思路

b. 部分匹配表

  • 先了解下什么是前缀,后缀
  • 部分匹配值:"前缀"和"后缀"的最长的共有元素的长度
  • "部分匹配"的实质

如何使用这个表来加速字符串的查找,以及这样用的道理是什么?

  • 如果在 j 处字符不匹配,那么由于前边所说的模式字符串 PMT 的性质, {主字符串} 中 i 指针之前的 PMT[j − 1] 位就一定与 {模式字符串} 的前 PMT[j−1] 位是相同的
  • 故 可直接省略前后缀那 n 位的比对;也就是说,虽然要重新比对了,但还没开始呢就已经完成字符串的部分匹配了(和暴力匹配比起来那算是赢在起跑线了 ...)
  • 接下来从 n+1 位的那个字符开始比对,前面已经利用 [最长前后缀] 直接完成了部分(n个字符)的匹配任务

c. next[] 生成

next[] 用于记录“模式串”每 1 个索引位置的最长公共前后缀,也就是标记实际字符串匹配过程中在某 1 位遇到不匹配的情况时,“模式串的”的 k 应该移动到的位置。

  • 首先模式串的第 0 位字符公共前后缀长度为 0
  • 进入循环,首先 i 在 1 的位置,k 在 0 的位置 (要注意 k 的位置和 next 数组下标没关系,k 是对于原来模式串而言的位置,现在在第 0 个 a 上)。如图,发现 p[i] = p[k],说明 i 位置最长公共前后缀为 1,记入 next[],ne[1] = 1。
  • 现在 k=1,在第二个 a 上。紧接着 i=2,发现 p[i]!=p[k],这时候怎么办呢?
    • 问问 k 前面的老兄:“你的公共前后缀是多少啊?”,k-1 说自己公共前后缀是 0
    • 那好吧,没得跳转了,直接 ne[2] = 0,下一个字符只能从头开始对了
  • 此时 i=3,k=0,发现一路顺风顺水
    • i=3 后面的字符 和 从头开始的模式串一直都匹配
    • 所以公共前后缀长度不断加 1,直到 i=8
  • 此时 k=5,而 p[i] != p[k]
    • 这时候又问问 k 前面的老兄的公共前后缀,k-1 说我的公共前后缀是 2
    • 说明k 前面的 2 个字符 和 打头的 2 个字符 是一样的;那么我们下一次直接从第 3 个字符匹配
  • 此时 k=2,i=8,发现还有 p[i] != p[k]
    • 再问问 k-1,k-1 说我的公共前后缀长度为 1
    • 说明 k 之前的那 1 个字符和开头的第 1 个字符是一样的,那么我们下一次直接从第 2 个字符匹配
  • 此时k=1, i=8。发现终于 p[i] == p[k],那么 ne[i] 等于多少呢?
    • k-1 的公共前后缀的长度加上这次匹配的字符个数 1,即 ne[8]=2;匹配完成~
  • [sum] 如果说KMP是一个模式串匹配主串的过程;那么,next生成过程就是模式串自己匹配自己的过程

2.3 代码实现

public class KMPAlgorithm {
    public static void main(String[] args) {
        String mainStr = "BBC ABCDAB ABCDABCDABDE";
        String pattern = "ABCDABD";
        int[] next = kmpNext(pattern);
        int index = kmpSearch(mainStr, pattern, next);
        System.out.println(index);
    }

    /**
     * 通过KMP算法在主串中查找模式串出现的位置
     * @param mainStr 主串
     * @param pattern 模式串
     * @param next 模式串的部分匹配表
     * @return 返回模式串首字符在主串中的索引; 如果没有, 返回-1
     */
    public static int kmpSearch(String mainStr, String pattern, int[] next) {
        for (int i = 0, j = 0; i < mainStr.length(); i++) {
            // i 不用动, 让 j 直接到最长前后缀的后一个字符位置
            while (j > 0 && mainStr.charAt(i) != pattern.charAt(j)) j = next[j-1];
            // 当前字符匹配,都往后移动一位
            if (mainStr.charAt(i) == pattern.charAt(j)) j++;
            // 匹配
            if (j == pattern.length()) return i - j + 1;
        }
        return -1;
    }


    // 获取一个字符串(子串)的部分匹配值表
    public static int[] kmpNext(String pattern) {
        int[] next = new int[pattern.length()];
        next[0] = 0;
        // 每一位的最长前后缀,i 索引着后缀,j 索引着前缀
        for (int i = 1, j = 0; i < pattern.length(); i++) {
            while (j > 0 && pattern.charAt(i) != pattern.charAt(j)) j = next[j-1];
            if (pattern.charAt(i) == pattern.charAt(j)) j++;
            next[i] = j;
        }
        return next;
    }
}

return i - ( j - 1 )

  • n 个字符,站在最后 1 个字符的位置上,要往前移动多少次,可以回到第一个字符?显而易见, n-1 次
  • kmpSearch~return 这里,匹配成功后,i 要回到(返回)和 模式串 相匹配的第 1 个字符的位置
  • 和上面是一样的道理,要往前滑动 n - 1 次,由于此时 j == n,所以是 i - (j - 1)
posted @ 2020-03-14 23:26  tree6x7  阅读(297)  评论(0编辑  收藏  举报