过千帆的记事本 记录点滴

KMP算法

2021-05-09
逸帅-吾爱破解

转自:https://www.52pojie.cn/thread-1412249-1-1.html

2、KMP算法

2.1、解决的问题

KMP是一个解决模式串在文本串是否出现过,如果出现过,找出最早出现的位置的经典算法,用来解决暴力匹配算法的不足

2.2、算法思路

2.2.1、部分匹配表的生成

什么是前缀与后缀

  • 前缀:ABCD的前缀为[A, AB, ABC]
  • 后缀:ABCD的后缀为[BCD, CD, D]

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

如下表:

kmp-示例模式串

2.2.2、匹配过程

问题:假设有一个字符串 str1= BBC ABCDAB ABCDABCDABDE,和一个子串 str2=ABCDABD。现在要判断 str1 是否含有 str2, 如果存在,就返回第一次出现的位置, 如果没有,则返回-1

步骤:

  1. 首先,用 str1的第一个字符和 str2的第一个字符去比较,不符合,关键词向后移动一位

    kmp-1

  2. 重复第一步,还是不符合,再后移

    kmp-2

  3. 一直重复,直到 Str1有一个字符与 Str2的第一个字符符合为止

    kmp-3

  4. 接着比较字符串和搜索词的下一个字符,还是符合

    kmp-4

  5. 遇到 Str1有一个字符与 Str2对应的字符不符合

    kmp-5

  6. 这时候不能像暴力匹配一样移动到最前面的下一个字符,因为前面的字符都匹配过了,当空格与 D 不匹配时,你其实知道前面六个字符是”ABCDAB”,这时候不要把”搜索位置”移回最前面的位置,而是根据匹配表,决定移动到哪个位置,这样就节省了效率,匹配表按照2.2.1的方法,进行计算

    kmp-6

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

    移动位数 = 已匹配的字符数 - 对应的部分匹配值,因为 6 - 2 等于 4, 所以将搜索词向后移动 4 位。

    kmp-7

  8. 此时C与空格不匹配,已经匹配的是“AB”两个字符,对应的”部分匹配值”为 0。

    所以,移动位数 = 2 - 0, 结果为 2, 于是将搜索词向后移 2 位

    kmp-8

  9. 继续按照字符,一个一个匹配,A与空格不匹配,后移一位,此时开始匹配

    kmp-9

  10. 逐位比较,C和D不匹配,此时前面匹配的依然是“ABCDAB”,按照前面第六步的方法,构建匹配表,可知要往后移动4位

    kmp-10

  11. 逐位匹配,此时发现完全匹配,代表找到了。

2.3、代码实现

import java.util.Arrays;

/**
 * home.php?mod=space&uid=686208 yishuai
 * @description kmp算法通俗易懂版
 * home.php?mod=space&uid=686237 2021/4/7 7:06 下午
 */
public class Kmp {
    public static void main(String[] args) {
        String str1 = "BBC ABCDAB ABCDABCDABDE";
        String str2 = "ABCDABD";
//        int[] next = getNext("ABCDABD");
//        System.out.println(Arrays.toString(next));
        int i = KmpSearch(str1, str2);
        System.out.println("找到的位置是:"+ i);

    }

    /**
     * 传入一个字符串,返回字符匹配表
     * home.php?mod=space&uid=952169 match 要生成匹配表的字符串
     * home.php?mod=space&uid=155549 返回匹配表数组
     */
    public static int[] getNext(String match){
        int[] next = new int[match.length()];
        //第一个字符没有前缀和后缀,所以必定是0
        next[0] = 0;
        //指向字符串的头部
        int j = 0,count=0;
        //从第二个元素,开始和第一个比较
        for (int i = 1; i < match.length(); i++) {
            if (match.charAt(j) != match.charAt(i)){
                count = 0;
            }
            //第一个元素和第i个元素匹配,代表前缀和后缀开始匹配
            if (match.charAt(j) == match.charAt(i)){
                next[i] = ++count;
                //这样子匹配第一个字符后,就会匹配第二个字符
                j++;
            }
        }
        System.out.println(Arrays.toString(next));
        return next;
    }

    /**
     * kmp算法字符匹配的部分
     * @param str1 源字符串
     * @param str2 要被匹配的字符串
     */
    public static int KmpSearch(String str1,String str2){
        //count匹配成功的次数
        int maxNum,count = 0;
        //字符匹配数组
        int[] next;
        //首先要进行暴力匹配,比对第一个字符是否相同
        for (int i = 0; i < str1.length(); i++) {

            //代表第一个元素匹配上了
            if (str2.charAt(count) == str1.charAt(i)){
                //此时是进行第下个元素的匹配
                count++;
                //如果全部匹配上了,代表找到了,就结束
                if(count == str2.length()){
                    //i跟着count一直往后走,所以要减去count,才是开头的位置,因为下标从0开始,所以要+1
                    return i - count + 1;
                }
            }else {
                if (count != 0){
                    //代表没匹配成功了,此时要把匹配成功的这一段截取下来,生成字符匹配表
                    String substring = str2.substring(0, count);
                    next = getNext(substring);
                    maxNum = next[0];
                    //得到最大的字符匹配
                    for (int j = 0; j < next.length; j++) {
                        if (next[j] > maxNum){
                            maxNum = next[j];
                        }
                    }
                    //移动位数 = 已匹配的字符数 - 对应的部分匹配值
                    //最后还要-1,是因为for循环的i要++
                    i += count - maxNum - 1;
                    //匹配次数清零
                    count = 0;
                }
            }
        }
        return -1;
    }
}

Similar Posts

Comments