【数据结构与算法】字符串匹配算法之暴力匹配算法、KMP匹配算法

1. 字符串匹配场景

有一个字符串sourceStr = “BBC ABCDAB ABCDABCDABDE”,和一个待匹配子字符串matchsStr = “ABCDABD”。如果sourceStr含有matchsStr, 则返回第一次匹配的第一个index, 如果不含则返回-1

2. 暴力匹配算法

2.1 暴力匹配算法介绍

假设现在sourceStr匹配到i位置,待匹配子字符串matchsStr匹配到j位置,则有:

  1. 如果当前字符匹配成功(即sourceStr[i] == matchsStr[j]),则i++,j++,继续匹配下一个字符
  2. 如果不匹配(即sourceStr[i] != matchsStr[j]),令i = i - j + 1,j = 0。相当于每次匹配失败时,i回溯到下一位继续匹配,j被置为0

2.2 暴力匹配算法程序实现

public class ViolenceMatch {

    public static void main(String[] args) {
        String sourceStr = "BBC ABCDAB ABCDABCDABDE";
        String matchStr = "ABCDABD";

        int matchIndex = violenceMatch(sourceStr, matchStr);
        System.out.println("matchIndex = " + matchIndex);
    }

    // 暴力匹配算法实现
    public static int violenceMatch(String sourceStr, String matchStr) {
        char[] sourceChars = sourceStr.toCharArray();
        char[] matchChars = matchStr.toCharArray();

        int sourceLength = sourceChars.length;
        int matchLength = matchChars.length;

        // i索引指向sourceChars
        int i = 0;
        // j索引指向matchChars
        int j = 0;

        while (i < sourceLength && j < matchLength) {
            // 如果匹配成功
            if (sourceChars[i] == matchChars[j]) {
                i++;
                j++;
            } else {
                // 如果没有匹配成功
                i = i - (j - 1);
                j = 0;
            }
        }

        // 当matchStr完全匹配成功之后,进行了j++,则j == matchLength
        if (j == matchLength) {
            // 返回第一次匹配的第一个index
            return i - j;
        } else {
            return -1;
        }
    }
}

运行程序,结果如下:

matchIndex = 15

3. KMP匹配算法

3.1 KMP匹配算法介绍

KMP算法的目的是减少不必要的匹配,提高匹配效率。KMP算法就是利用matchStr形成一个部分匹配表,通过一个partMatch数组,保存matchStr中,截止当前字符形成的字符串,的前后缀最大公共字符串的长度

每次回溯时,通过partMatch数组,不断的迭代,以便重新找到matchStr的前缀,和sourceStr中截止当前字符形成的字符串的后缀的最大公共字符串,然后继续进行匹配

3.2 KMP匹配算法实现的基础知识

3.2.1 前缀和后缀的概念

对于字符串"bread"

前缀有:b、br、bre、brea
后缀有:read、ead、ad、d

3.2.2 前缀和后缀的最大公共字符串长度

以"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

3.2.3 部分匹配表

部分匹配表就是,matchStr截止当前字符形成的字符串,的前缀和后缀的最大公共字符串的长度。如下所示:

字符串 A B C D A B D
部分匹配值 0 0 0 0 1 2 0

3.3 KMP匹配算法程序实现

import java.util.Arrays;

public class KMPMatch {

    public static void main(String[] args) {
        String sourceStr = "BBC ABCDAB ABCDABCDABDE";
        String matchStr = "ABCDABD";

        int[] partMatchArray = getPartMatchArray(matchStr);
        System.out.println("partMatchArray = " + Arrays.toString(partMatchArray));

        int index = kMPMatch(sourceStr, matchStr, partMatchArray);
        System.out.println("index = " + index);
    }

    // 获取matchStr部分匹配值表
    public static int[] getPartMatchArray(String matchStr) {
        // 保存部分匹配表的数据
        int[] partMatchArray = new int[matchStr.length()];

        // 第一个字符的前后缀都为空,最大公共字符串长度为0
        partMatchArray[0] = 0;

        // 从第二个字符开始进行遍历处理。并定义一个指针prefixPointer指向matchStr的第一个字符, 用于前缀匹配
        for (int suffixPoint = 1, prefixPoint = 0; suffixPoint < matchStr.length(); suffixPoint++) {

            // 对于一个新处理的字符,如果匹配不成功,则需要找出后缀和后缀新的公共字符串,再在新的公共字符串上进行匹配
            while (prefixPoint > 0 && matchStr.charAt(prefixPoint) != matchStr.charAt(suffixPoint)) {
                // 这里举例说明:
                // 例如"ABACABAD", 当前处理字符为"D", prefixPoint指向字符"C"
                // "ABA"为处理上一个字符"A"的最大公共字符串
                // 从"ABA"中找到新的公共字符串"A",即找到的"A"是下一次前缀匹配的前一部分,找到的"A"也是下一次后缀匹配的前一部分
                prefixPoint = partMatchArray[prefixPoint - 1];
            }

            // 在公共字符串(前缀的前一部分,后缀的前一部分)的基础上,对于一个新处理的字符,如果匹配成功,则公共字符串长度加1
            if (matchStr.charAt(prefixPoint) == matchStr.charAt(suffixPoint)) {
                // 前缀的指针进行后移(公共字符串变长),后缀的指针在for循环中进行后移
                prefixPoint++;
            }

            // 保存当前处理字符的公共字符串长度
            partMatchArray[suffixPoint] = prefixPoint;
        }

        return partMatchArray;
    }

    // KMP匹配算法实现
    public static int kMPMatch(String sourceStr, String matchStr, int[] partMatchArray) {

        // 遍历sourceStr进行处理。并定义一个指针matchPointer指向matchStr的第一个字符
        for (int sourcePointer = 0, matchPointer = 0; sourcePointer < sourceStr.length(); sourcePointer++) {

            // 对于一个新处理的字符,如果匹配不成功,则需要将matchPointer进行前移
            while (matchPointer > 0 && sourceStr.charAt(sourcePointer) != matchStr.charAt(matchPointer)) {
                // 这里举例说明:
                // 例如sourceStr = "ABACABAD", sourcePointer指向字符"C"
                //     matchStr = "ABAD",      matchPointer指向字符"D"
                // "ABA"为已经匹配过的公共字符串
                // 从"ABA"中找到新的公共字符串"A",即sourceStr的"ABA"的最后一个”A“,和matchStr的”ABA“的第一个”A“重叠
                // sourceStr的”ABA“的最后一个"A”不用重新匹配,matchStr的matchPointer直接指向"ABA"的“B”,让sourceStr的“C”和matchStr的“B"进行匹配即可
                matchPointer = partMatchArray[matchPointer - 1];
            }

            // 对于一个新处理的字符,如果匹配成功,则matchPointer后移,sourcePointer在for循环中进行后移
            if (sourceStr.charAt(sourcePointer) == matchStr.charAt(matchPointer)) {
                matchPointer++;
            }

            // 表示matchStr所有字符全部匹配到了,找到了
            if (matchPointer == matchStr.length()) {
                // 返回第一次匹配的第一个index
                return sourcePointer - (matchPointer - 1);
            }
        }

        // 没有匹配到, 返回-1
        return -1;
    }

}

运行程序,结果如下:

partMatchArray = [0, 0, 0, 0, 1, 2, 0]
index = 15

猜你喜欢

转载自blog.csdn.net/yy8623977/article/details/127262385
今日推荐