子字符串查找算法

术语:模式和文本

在文本中找出和模式相符的子串。

子字符串查找算法:

算法 性能 备注
暴力查找法 一般情况:O(N+M);极端情况:O(NM) 实现简单
KMP 算法 O(N+M) 确定有限状态自动机 DFA
BM 算法 一般只检查文本部分字符 从右往左比较模式串
RK 算法 O(N+M) 哈希算法、实现简单

暴力子字符串查找算法

性能:

  • 在极端情况下,时间复杂度是 O(MN) - (文本和模式存在大量重复字符)
  • 一般情况下,时间复杂度是 O(M + N) - (一般不需要完整比较模式串)

思路:

  • 枚举文本中所有和模式串长度相等的子串并进行比较
  • 枚举子串时,如果文本剩余长度小于模式长度时,可提前结束查找

算法实现1:

public class BruteForce1 {
    /**
     * 在文本中查找和模式相符的子串
     * @param pat pattern
     * @param txt text
     * @return 子串存在,返回字符首字符索引;否则,返回 -1
     * */
    public static int search(String pat, String txt) {
        int m = pat.length();
        int n = txt.length();
        for (int i = 0; i <= n - m; i++) {
            int j;
            for (j = 0; j < m; j++) {
                if (pat.charAt(j) != txt.charAt(i + j)) {
                    break;
                }
            }
            if (j == m) {
                return i;
            }
        }
        return -1;
    }
}

测试:

class BruteForce1Test {
    @Test
    void search() {
        assertEquals(0, BruteForce1.search("Hello", "Hello World"));
        assertEquals(6, BruteForce1.search("World", "Hello World"));
        assertEquals(-1, BruteForce1.search("not exists", "Hello World"));
    }
}

思路:将文本与模式进行比较,比较失败时回退指针 i, j,对下一个子串进行比较

算法实现2:

public class BruteForce2 {
    public static int search(String pat, String txt) {
        int m = pat.length();
        int n = txt.length();
        int i, j;
        for (i = 0, j = 0; i < n && j < m; i++) {
            if (txt.charAt(i) == pat.charAt(j)) {
                // 匹配时,比较下一个字符
                j++;
            } else {
                // 不匹配时,回退指针 i, j
                i -= j;
                j = 0;
            }
        }
        if (j == m) {
            return i - m;
        }
        return -1;
    }
}

测试:

class BruteForce2Test {
    @Test
    void search() {
        assertEquals(0, BruteForce2.search("Hello", "Hello World"));
        assertEquals(6, BruteForce2.search("World", "Hello World"));
        assertEquals(-1, BruteForce2.search("not exists", "Hello World"));
    }
}

总结:

  • 性能问题;在极端情况下,指针 i, j 总是需要回退 m - 1 个位置,存在大量重复的比较
  • 应用情况:在一般情况下,不需要完整地比较整个子串且实现简单

KMP 算法

本节重点:

  • DFA 如何使用
  • DFA 如何构造

全称:Knuth-Morris-Pratt 子字符串查找算法(三位发明者的名字)

基本思想:在匹配失败时,能够利用已比较的信息,将 j 重置使 i 不回退

可能性:当匹配失败时,可以通过右移模式串来避免回退指针 i

  • 右移时,如果已比较的子串(后缀)和已比较的模式串(前缀)能重叠,则将模式串移到该位置
  • 否则,将模式串移到和下一个子串重叠的位置
txt: AABAAB|AAAB
pat: AABAAA|
  ->    AAB|AAA

实现:对模式串进行预处理,构造 DFA(确定有限状态自动机),在遍历文本串的字符时,可以从 DFA 得到下一个 j 的值(前进或回退)

DFA: dfa[txt.chatAt(i)][j] 的值是和文本串的下一个字符 txt.chatAt(i+1) 比较的 j 值

过程:将文本字符输入到 DFA,DFA 会从一个状态转换到另一个状态,直到达到终止状态(匹配成功)或文本结束(匹配失败)

算法实现:

public class KMP {
    private static final int R = 256; // 字符集大小
    private String pattern;
    private int m;
    private int[][] dfa;

    /**
     * 根据模式串构造确定有限状态自动机 DFA
     * */
    public KMP(String pattern) {
        this.pattern = pattern;
        this.m = pattern.length();
        dfa = new int[R][m];
        // 初始化第一列
        dfa[pattern.charAt(0)][0] = 1;
        // 初始化其他的列,从第二列开始
        // 当 txt[i..i+j] 匹配失败时,从 txt[i+1] 开始匹配,
        // 这里可以想象成DFA正在处理第 2 个字符的匹配情况
        // X 记录重启状态
        for (int j = 1, X = 0; j < m; j++) {
            for (int c = 0; c < R; c++) {
                dfa[c][j] = dfa[X][j]; // 匹配失败,将 dfa[][X] 复制到 dfa[][j]
            }
            dfa[pattern.charAt(j)][j] = j + 1; // 匹配成功,将 dfa[pattern.charAt(j)][j] 设置成 j + 1
            X = dfa[pattern.charAt(j)][X]; // 更新 X,即从 txt[i+1] 开始匹配或者说在构建自动的同时也在使用自动机
        }
    }

    /**
     * 在文本串中查找子串
     * 将文本串的字符逐个放到 DFA 中,直到 DFA 达到终止状态或文本串结束
     * */
    public int search(String txt) {
        int n = txt.length();
        int i, j;
        for (i = 0, j = 0; i < n && j < m; i++) {
            j = dfa[txt.charAt(i)][j];
        }
        if (j == m) {
            return i - m;
        }
        return -1;
    }
}

测试:

class KMPTest {
    @Test
    void search() {
        KMP kmp = new KMP("Hello");
        Assertions.assertEquals(0, kmp.search("Hello World"));
        Assertions.assertEquals(4, kmp.search("Say Hello"));
        Assertions.assertEquals(-1, kmp.search("hello world"));
    }
}

应用场景:

  • 文本串和模式串的重复性很高
  • 文本串是长度不确定的输入流
posted @ 2022-08-13 16:22  廖子博  阅读(113)  评论(0编辑  收藏  举报