KMP算法

113 阅读4分钟

1. 字符串匹配问题

  1. 有一个字符串 str1="BBC ABCDAB ABCDABCDABDE",和子串 str2="ABCDABD"
  2. 判断 str1 是否含有 str2 ,如果存在,就返回第一次出现的位置,如果没有,则返回-1

2. 暴力匹配算法

2.1 思路分析

假设,str1匹配到了 i 位置,子串 str2 匹配到了 j 位置,则有:

① 如果当前字符匹配成功(即:str1[i] = str2[j]),则 i++, j++, 继续匹配下一个字符

② 如果失配(即 str1[i] != str2[j]),令主串和子串回溯:i=i-(j-1),j=0;

③ 用暴力匹配解决,会有大量的回溯,每次只能移动一位,若是不匹配,移动到下一位接着判断,浪费了大量的时间

2.2 代码实现

public class ViolenceMatch {
    public static void main(String[] args) {
        String str1 = "爱爱你 我爱你他我爱 我爱你他我爱你他我爱他他了";
        String str2 = "我爱你他我爱他~";
        int index = violenceMatch(str1,str2);
        System.out.println("index=" + index);
    }

    //暴力匹配算法的实现
    public static int violenceMatch(String str1,String str2){
        char[] s1 = str1.toCharArray();
        char[] s2 = str2.toCharArray();

        int s1Len = s1.length;
        int s2Len = s2.length;

        int i = 0; //i索引指向s1
        int j = 0; //j索引指向s2

        while(i < s1Len && j < s2Len){ //保证匹配时,不越界
            if(s1[i] == s2[j] ){ //匹配成功
                i++;
                j++;
            }else{ //没有匹配成功
                //如果失配(即 str1[i] != str2[j] ,令 i=i-(j-1),j=0)
                i = i - (j - 1);
                j = 0;
            }
        }
        //判断是否匹配成功
        if(j == s2Len) {
            return i - j;
        }else{
            return -1;
        }
    }
}

3. KMP算法

  • Knuth-Morris-Pratt 字符串查找算法,简称 “KMP算法” ,常用于在一个文本串 S 内查找一个模式串 P 的出现位置。
  • KMP 算法利用之前就判断过的信息,通过一个 next 数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过next 数组找到,前面匹配的过的位置,省去了大量的计算时间

3.1 关于KMP中部分匹配表

  • 是在子串匹配主串时,发生不匹配时,但前面已经比较过的字符串已经大致知道,为了避免主串的过度回溯,所以需要部分匹配表,调整回溯的位置

3.1.1 关于前缀和后缀

image-20220409215418893

3.1.2 部分匹配表

  • 部分匹配表:就是“前缀”和“后缀”的最长的共有元素的长度。以 “ABCDABD” 为例

A:前缀和后缀都为空集,共有元素长度为0

AB: 前缀为【A】,后缀为【B】,共有元素长度为 0

ABC: 前缀为 【A,AB】,后缀为【C,BC】,共有元素长度为0

ABCD:前缀为【A,AB,ABC】,后缀为【D,CD,BCD】,共有元素长度为0;

ABCDA:前缀为【A,AB,ABC,ABCD】,后缀为【A,DA,CDA,BCDA】,共有元素为【A】,长度为1

ABCDAB :前缀为【A,AB,ABC,ABCD,ABCDA】,后缀为【B,AB,DAB,CDAB,BCDAB】,共有元素为【AB】,长度为2;

ABCDABD:前缀为【A,AB,ABC,ABCD,ABCDA,ABCDAB】,后缀为【D,BD,ABD,DABD,CDABD,BCDABD】,共有元素长度为0;

image-20220409220835901

3.1.3 部分匹配的实质

  • 有时候,字符串的头部和尾部会有重复,比如 “ABCDAB" 之中有两个 ”AB“ ,那么它的 “部分匹配值”就是2 .搜索词移动的时候,第一个 “AB” 向后移动4位移动位数 = 已匹配的字符数 - 对应的部分的匹配值 ,就可以来到第二个 “AB” 的位置

3.2 思路分析

① 首先,用 Str1 的第一个字符和 Str2 的第一个字符去比较,不符合,Str1的指针向后移动一位

image-20220409213817126

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

image-20220409214003357

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

image-20220409214203375

④ 接着比较字符串和搜索词的下一个字符,若是符合,继续比较下一个字符,重复操作,若是遇到不符合的

image-20220409214411261

⑤ 当发现不匹配时,因为BCD已经比较过了,没有必要再做重复的工作。而当空格与D不匹配时,其实已经直到了前面六个字符位“ABCDAB”。KMP的算法思想是,设法利用这个已知信息,不要回溯到已经比较过的位置。

image-20220409214847765

⑥ 对 Str2 计算部分匹配值

image-20220409220835901

⑦ 已知空格与 D 不匹配,前面六个字符“ABCDAB”是匹配的。查表可知,最后一个匹配字符串B对应的部分匹配值为2,因此:

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

⑧ 因为空格和C不匹配,搜索词还要继续往后移,这是已匹配的字符数位 “AB”,对应的 “部分匹配值” 为0,所以移动位数 = 2 - 0;

于是将搜索词先把后移动 2 位

image-20220409221615971

⑨ 因为 空格与A不匹配,继续向后移一位

image-20220409221731350

⑩ 逐位比较,知道发现 C 与 D 不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动 4 位

image-20220409222112492

  1. 逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成

image-20220409222344624

3.3 代码实现

import java.util.Arrays;

/**
 * @author feng
 * @create 2022-03-21 8:03
 */
public class KMPAlgorithm {
    public static void main(String[] args) {
        String str1 = "BBC ABCDAB ABCDABCDABDE";
        String str2 = "ABCDABD";
//        String str2 = "BBC";

        int[] next = kmpNext(str2); //[0,1]
        System.out.println("next=" + Arrays.toString(next));

        int index = kmpSearch(str1,str2,next);
        System.out.println("index= "+index);

    }

    //写出我们的kmp搜索算法
    /**
     *
     * @param str1 原字符串
     * @param str2 子字符串
     * @param next 部分匹配表
     * @return 如果是-1就是没有匹配到,否则返回第一个匹配到的位置
     */
    public static int kmpSearch(String str1,String str2,int[] next){
        //遍历
        for (int i = 0,j = 0; i < str1.length(); i++) {
            //需要处理 str1.charAt(i) != str2.charAt(j),去调整j的大小
            //KMP核心算法点
            while(j > 0 && str1.charAt(i) != str2.charAt(j)){
                j = next[j-1];
            }
            if(str1.charAt(i) == str2.charAt(j)){
                j++;
            }
            if(j == str2.length()){ //找到了i=2 j = 3
                return i - j + 1;
            }
        }
        return  -1;
    }

    //获取到一个字符串(子串)的部分匹配值表
    // 如果子串中 ABCDABD
    //A->0
    // AB:  正A 逆B 0
    //ABC: 正 A AB 逆C BC 0
    //ABCD : 正 A AB ABC 逆 C BC BCD 0
    //ABCDA: 正 A AB ABC ABCD 逆 A DA CDA BCDA 1(A)
    //ABCDAB: 正 A AB ABC ABCD ABCDA 逆 B AB DAB CDAB BCDAB ->2(AB)
    //ABCDABD: 0
    public static  int[] kmpNext(String dest){
        //创建一个next数组,保存部分匹配值
        int[] next = new int[dest.length()];
        next[0] = 0; //如果字符串是长度为1 部分匹配值是0
        for (int i = 1,j=0; i < dest.length(); i++) {
            //当dest.charAt(i) != dest.charAt(j) 我们需要从 next[j-1] 获取新的j
            //直到我们发现有dest.charAt(i) == dest.charAt(j) 成立时才退出
            //这是kmp算法的核心点
            while(j > 0 && dest.charAt(i) != dest.charAt(j) ){
                j = next[j-1];
            }
            //当dest.charAt(i) == dest.charAt(j) 满足时,部分匹配值+1
            if(dest.charAt(i) == dest.charAt(j)){
                j++;
            }
            next[i] = j;
        }
        return next;
    }
}