zoukankan      html  css  js  c++  java
  • 40-暴力匹配 & KMP算法

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

    暴力匹配

    《算法(第4版)》

    思路分析

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

    代码实现

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

    KMP算法

    算法简述

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

    思路分析

    通过案例整理思路

    部分匹配表

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

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

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

    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生成过程就是模式串自己匹配自己的过程

    代码实现

    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)
  • 相关阅读:
    HDU 3152 Obstacle Course(BFS+优先队列 重载)
    芸芸毕业生
    shell学习三十四天----printf具体解释
    tomcat启动批处理——catalina.bat
    ZooKeeper启动过程2:FastLeaderElection
    R语言——数据分析的一把利剑
    Oracle blob字段的插入和更新
    [LeetCode] 698. Partition to K Equal Sum Subsets
    小知识!
    小知识!
  • 原文地址:https://www.cnblogs.com/liujiaqi1101/p/12489508.html
Copyright © 2011-2022 走看看