串的模式匹配——Brute-Force与KMP算法

Brute-Force算法

  • Brute-Force算法简称BF算法,也叫简单匹配算法或暴力匹配算法,采用穷举法,其基本思想为:从目标串str="aabaabaaf"的第一个字符开始和模式串substr="aabaaf"中的第一个字符相比较。若相等,则继续逐个比较后续字符;否则从目标串str的第二个字符开始重新与模式串substr中的第一个字符比较。以此类推,若从目标串str的第i个字符开始,每个字符依次与模式串substr中的对应字符相等,则匹配成功,该算法返回位置i——表示此时substr的第一个字符在str中出现的下标;否则,匹配失败,说明substr不是str的子串,返回-1
  • 它对应的代码为:
package com.cdc.demo;

public class BruteForceDemo {

    public static void main(String[] args) {
        String str="aabaabaaf";
        String substr="aabaaf";
        System.out.println("index="+bruteForce(str,substr));
        System.out.println("index="+str.indexOf(substr));
    }

    public static int bruteForce(String str,String subStr){
        int i=0;
        int j=0;
        char[] s1 = str.toCharArray();
        char[] s2 = subStr.toCharArray();
        //确保不越界
        while (i<s1.length && j<s2.length){
            if (s1[i] == s2[j]){
                i++;
                j++;
            }else {
                //回退str的索引
                i=i-(j-1);
                j=0;
            }
        }
        if (j >= s2.length){
            //返回子串substr第一次出现的位置
            return i-j;
        }
        return -1;
    }
}

其运行结果为:

"C:\Program Files\Java\jdk1.8.0_191\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2018.2.5\lib\idea_rt.jar=1026:C:\Program Files\JetBrains\IntelliJ IDEA 2018.2.5\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_191\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\rt.jar;C:\Users\admin\IdeaProjects\demo\out\production\demo" com.cdc.demo.BruteForceDemo
index=3
index=3

Process finished with exit code 0

  • 在Brute-Force算法中,有一个很明显的缺点,就是它在匹配到不相同的字符时,会回溯目标串str的索引,并且每次都是回溯到上一次的后面一个位置。假如目标串的长度为m、子串的长度为n,最坏情况下,每次都是最后一个字符没有找到相对应的字符,那么它每次子串扫描的长度就是n个,每次回溯到目标串都需要花费扫描n个字符串的时间,那么m个字符中,它需要回溯的此时就是m*n ,它的时间复杂度就是:O(m*n),这是相当浪费时间的。

KMP算法

  • Brute-Force算法中,我们了解到它所对应的时间复杂度为O(m*n),每次扫描不相等的字符,都会导致主串回溯到上一次扫描的位置。但是对于KMP而言,它并不是每次扫描到不相等的元素就会导致主串回溯到上一次扫描的位置,而是会去KMP算法中很重要的一个信息:前缀表中查询扫描失败字符的前面一个位置在该表中对应的值,从这里重新开始扫描。

在KMP算法中最重要的内容之一:前缀表

  • 在了解前缀表之前,我们需要先了解几个概念:前缀后缀最长相等前后缀

    • 前缀: 包含首字符,但是不包含尾字符的子串
    • 后缀: 包含尾字符,但是不包含首字符的字串
    • 相等前后缀: 在前缀和后缀中,相等的子串
  • 以模式串:aabaaf为例,它的前缀和后缀分别有:

    • a只有一个字符,它没有前缀也没有后缀,所以也没有相等前后缀,它的最长相等前后缀长度=0
    • aa它的前缀有:{a},后缀有:{a},它的相等前后缀有:{a},它的最长相等前后缀长度=1
    • {aab}它的前缀有:{a、aa},后缀有:{ab、b},它没有相等前后缀,它的最长相等前后缀长度=0
    • {aaba}它的前缀有:{a、aa、aab},后缀有:{aba、ba、a},它的相等前后缀有:{a},它的最长相等前后缀长度=1
    • {aabaa}它的前缀有:{a、aa、aab、aaba},后缀有:{abaa、baa、aa、a},它的相等前后缀有:{a、aa},它的最长相等前后缀长度=2
    • {aabaaf}它的前缀有:{a、aa、aab、aaba、aabaa},它的后缀有:{abaaf、baaf、aaf、af、f},它没有相等前后缀,它的最长相等前后缀长度=0
  • 前缀表就是模式串中各个字符它们的最长相等前后缀长度组成的一个表格,所以模式串aabaaf所对应的前缀表就是:
    前缀表

    • 在这个表中,上面一行表示的是数组的索引
    • 下面一行表示的是它对应的前缀表中的值,也就是各个最长相等前后缀的长度
  • 以目标串str="aabaabaaf"和模式串substr="aabaaf"为例,它们对应的索引分别为:ij。在匹配的过程中,分为两个过程:

    • str[i]substr[j]的值不同时: 模式串会去寻找在未匹配到相等字符前面一位在前缀表中对应的值,并将j改变到它在next数组也就是前缀表中的前一位的值,即j=next[j-1],此时i不变,继续比较str[i]substr[j]的位置,如果它们不相等,则继续改变j的位置。
    • str[i]substr[j]的值相同时: 改变ij的位置,继续往后面匹配。
    • 举例: 模式串从字符第一个字符a开始,它与str中的字符都是相对应的,所以i与j是同步改变的。但是 当模式串匹配到f的位置时,它与目标串没有相对应的字符,所以这时候需要改变j的值,将它回溯到前缀表中j-1的位置,也就是next[5-1]所对应的值,即j=next[j-1]=2。与此同时i不会有所变化,str[i=5]='b',substr[j=2]=b,所以进入到相同情况下,继续匹配。直到匹配到字符串str的末尾,判断j >= substr.length是否成立,如果成立,则说明j遍历substr完成,并且它与str都有对应的字符,所以此时需要返回substr字符首字符在目标串str中第一次出现的位置,也就是i-(j-1);否则返回-1
      前缀表
  • 代码:

package com.cdc.algorithm.common;

import java.util.Arrays;

/**
 * @author cdc
 * @email c925638766@163.com
 * @date 2022/4/15 20:29
 */
public class StringSearchDemo {

    public static void main(String[] args) {
        String str = "aabaabaaf";
        String substr = "aabaaf";
        int[] next = generateNext(substr);
        System.out.println("aabaaf对应的next数组为:" + Arrays.toString(next));
        System.out.println("aabaaf在字符串aabaabaaf中的索引位置为:" + kmpSearch(str, substr, next));

        str = "aabaacaabaabaaf";
        System.out.println("\n---------------字符aabaacaabaabaaf-------------------------");
        System.out.println("aabaaf对应的next数组为:" + Arrays.toString(next));
        System.out.println("aabaaf在字符串aabaacaabaabaaf中的索引位置为:" + kmpSearch(str, substr, next));

        str = "aabaacaabaabaac";
        System.out.println("\n---------------字符aabaacaabaabaac-------------------------");
        System.out.println("aabaaf对应的next数组为:" + Arrays.toString(next));
        System.out.println("aabaaf在字符串aabaacaabaabaac中的索引位置为:" + kmpSearch(str, substr, next));
    }


    /**
     * 根据所传入的模式串生成其对应的前缀表
     *
     * @param substr 模式串
     * @return 生成好的前缀表
     */
    public static int[] generateNext(String substr) {
        int[] next = new int[substr.length()];
        //在next数组中,0对应着首字符的最长前后缀长度,首字符没有前缀和后缀,所以直接令它=0
        next[0] = 0;
        char[] chars = substr.toCharArray();
        //i表示的是后缀的末尾位置,j表示的是前缀的末尾位置
        for (int i = 1, j = 0; i < chars.length; i++) {
            //前后缀不相同的情况,需要回退j的位置,将j回退到next数组的上一位
            //j>0避免数组越界,并且可能不止需要回退一次,所以需要用whileu循环
            while (j > 0 && chars[i] != chars[j]) {
                //回退j的值
                j = next[j - 1];
            }
            //如果前缀和后缀的值相等,需要将j后移一位
            //以字符"aabaaf"为例,初值i=1,j=0,此时chars[i]=a,chars[j]=a,则此时我们就需要将j后移一位
            //将j++后,我们需要更新next数组的值,由于i是循环变量会在for循环中更改,所以我们不用管
            if (chars[i] == chars[j]) {
                j++;
            }
            next[i] = j;
        }
        return next;
    }

    /**
     * kmp算法匹配字符串中子串的位置
     * 为了方便,此处直接采用String中的charAt()方法来判断索引位置对应的字符
     * @param str   目标串
     * @param substr    子串
     * @param next  模式串对应的前缀表
     * @return  返回子串在目标串的位置,不存在则返回-1
     */
    public static int kmpSearch(String str, String substr, int[] next) {
        int index = -1;

        for (int i = 0, j = 0; i < str.length(); i++) {
            //当str[i] != substr[j]情况成立时,将j回溯到前缀表中它前面的一给位置,即j=next[j-1]
            //j > 0为了避免数组越界
            while (j > 0 && str.charAt(i) != substr.charAt(j)) {
                j = next[j - 1];
            }

            //当目标串str中i的位置和模式串substr中j的位置字符相同时,移动模式串j的位置,进行下一次比较
            if (str.charAt(i) == substr.charAt(j)) {
                j++;
            }
            //如果j移动了超过模式串的位置,则说明直到最后一个字符,目标串和模式串的字符都是一致的
            // 否则j++得到的值不会比子串的长度大
            if (j >= substr.length()) {
                System.out.println("j="+j+"substr.length="+substr.length());
                index = i - (j - 1);
                break;
            }
        }
        return index;

    }
}

  • 运行结果:
"C:\Program Files\Java\jdk1.8.0_202\bin\java.exe" "-javaagent:F:\JetBrains\IntelliJ IDEA 2020.2.4\lib\idea_rt.jar=56916:F:\JetBrains\IntelliJ IDEA 2020.2.4\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_202\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar;F:\Java自学\数据结构\Java_DataStructure\out\production\Java_DataStructure;F:\Maven_jars\org\projectlombok\lombok\1.18.16\lombok-1.18.16.jar" com.cdc.algorithm.common.StringSearchDemo
aabaaf对应的next数组为:[0, 1, 0, 1, 2, 0]
aabaaf在字符串aabaabaaf中的索引位置为:3

---------------字符aabaacaabaabaaf-------------------------
aabaaf对应的next数组为:[0, 1, 0, 1, 2, 0]
aabaaf在字符串aabaacaabaabaaf中的索引位置为:9

---------------字符aabaacaabaabaac-------------------------
aabaaf对应的next数组为:[0, 1, 0, 1, 2, 0]
aabaaf在字符串aabaacaabaabaac中的索引位置为:-1

Process finished with exit code 0

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值