每日算法 — 使用java实现Boyer-Moore:坏字符跳转与好后缀规则优化

Boyer-Moore算法是经典的高效字符串匹配算法,通过预处理模式串构建坏字符表与好后缀表,实现大幅跳跃跳过不可能匹配的位置。本文用Java完整实现BM算法,讲解两种启发式规则的构建与协同工作原理。

算法核心思想

BM算法从模式串末尾向前比较。当不匹配时,利用两种启发式规则决定模式串右移距离:
– 坏字符规则:根据文本中不匹配的字符在模式串中的位置决定跳跃
– 好后缀规则:根据已匹配的后缀在模式串中的其他出现位置决定跳跃

取两者较大值作为实际跳跃距离。

public class BoyerMoore {
    private final String pattern;
    private final int[] badChar; // 坏字符表
    private final int[] goodSuffix; // 好后缀表

    public BoyerMoore(String pattern) {
        this.pattern = pattern;
        this.badChar = buildBadCharTable(pattern);
        this.goodSuffix = buildGoodSuffixTable(pattern);
    }

坏字符表构建

    // 坏字符表:记录每个字符在模式串中最右出现位置
    // 大小256覆盖ASCII字符集
    static int[] buildBadCharTable(String pattern) {
        int[] table = new int[256];
        // 初始化为-1表示未出现
        for (int i = 0; i < 256; i++) table[i] = -1;
        for (int i = 0; i < pattern.length(); i++) {
            table[pattern.charAt(i)] = i; // 记录最右位置
        }
        return table;
    }

    // 坏字符跳跃距离:若字符不在模式串中,跳跃m;否则跳跃 max(1, j - lastPos)
    int badCharShift(char c, int j) {
        int lastPos = badChar[c];
        // j为当前不匹配位置(从右数)
        return Math.max(1, j - lastPos);
    }

好后缀表构建

    // 好后缀表:当后缀已匹配但前面不匹配时,模式串应右移多少
    static int[] buildGoodSuffixTable(String pattern) {
        int m = pattern.length();
        int[] suffix = new int[m]; // suffix[i]表示以i结尾的后缀与模式串后缀的最长匹配长度
        int[] gs = new int[m];     // 好后缀跳跃表

        // 计算suffix数组
        suffix[m - 1] = m;
        for (int i = m - 2; i >= 0; i--) {
            int j = i;
            while (j >= 0 && pattern.charAt(j) == pattern.charAt(m - 1 - i + j)) {
                j--;
            }
            suffix[i] = i - j;
        }

        // 情况1:好后缀在模式串中有其他完整出现
        for (int i = 0; i < m; i++) gs[i] = m;
        int j = 0;
        for (int i = m - 1; i >= 0; i--) {
            if (suffix[i] == i + 1) {
                for (; j < m - 1 - i; j++) {
                    if (gs[j] == m) gs[j] = m - 1 - i;
                }
            }
        }

        // 情况2:好后缀的后缀是模式串的前缀
        for (int i = 0; i <= m - 2; i++) {
            gs[m - 1 - suffix[i]] = m - 1 - i;
        }

        return gs;
    }

完整搜索算法

    // BM字符串匹配:返回所有匹配起始位置
    public java.util.List<Integer> search(String text) {
        java.util.List<Integer> matches = new java.util.ArrayList<>();
        int n = text.length();
        int m = pattern.length();
        if (m == 0 || n < m) return matches;

        int i = 0; // text中的对齐位置
        while (i <= n - m) {
            int j = m - 1; // 从模式串末尾开始比较

            // 从右向左比较
            while (j >= 0 && pattern.charAt(j) == text.charAt(i + j)) {
                j--;
            }

            if (j < 0) {
                // 完全匹配
                matches.add(i);
                // 继续搜索:使用好后缀规则移动
                i += goodSuffix[0];
            } else {
                // 不匹配发生在位置j
                char bad = text.charAt(i + j);
                int bcShift = badCharShift(bad, j);
                int gsShift = goodSuffix[j];
                // 取两种规则的最大跳跃值
                i += Math.max(bcShift, gsShift);
            }
        }
        return matches;
    }

主程序与性能对比

    public static void main(String[] args) {
        // 测试用例1:英文文本
        String text = "ABAAABCDABCABCABCDABCDABAAABCD";
        String pattern = "ABCD";
        BoyerMoore bm = new BoyerMoore(pattern);
        System.out.println("文本: " + text);
        System.out.println("模式: " + pattern);
        System.out.println("匹配位置: " + bm.search(text));

        // 测试用例2:长文本性能对比
        StringBuilder longText = new StringBuilder();
        for (int i = 0; i < 1000000; i++) {
            longText.append((char) ('a' + (i % 26)));
        }
        String lt = longText.toString();
        String pat = "xyzabc";

        // BM算法
        long t1 = System.currentTimeMillis();
        BoyerMoore bm2 = new BoyerMoore(pat);
        java.util.List<Integer> bmResult = bm2.search(lt);
        long t2 = System.currentTimeMillis();
        System.out.println("\nBM搜索耗时: " + (t2 - t1) + "ms, 匹配数: " + bmResult.size());

        // KMP算法对比
        t1 = System.currentTimeMillis();
        java.util.List<Integer> kmpResult = kmpSearch(lt, pat);
        t2 = System.currentTimeMillis();
        System.out.println("KMP搜索耗时: " + (t2 - t1) + "ms, 匹配数: " + kmpResult.size());

        // 暴力算法对比
        t1 = System.currentTimeMillis();
        java.util.List<Integer> bruteResult = bruteSearch(lt, pat);
        t2 = System.currentTimeMillis();
        System.out.println("暴力搜索耗时: " + (t2 - t1) + "ms, 匹配数: " + bruteResult.size());
    }

    // KMP算法实现(用于对比)
    static java.util.List<Integer> kmpSearch(String text, String pattern) {
        java.util.List<Integer> result = new java.util.ArrayList<>();
        int n = text.length(), m = pattern.length();
        int[] lps = new int[m];
        int len = 0;
        for (int i = 1; i < m; i++) {
            while (len > 0 && pattern.charAt(i) != pattern.charAt(len)) len = lps[len - 1];
            if (pattern.charAt(i) == pattern.charAt(len)) lps[i] = ++len;
        }
        int i = 0, j = 0;
        while (i < n) {
            if (text.charAt(i) == pattern.charAt(j)) { i++; j++; }
            if (j == m) { result.add(i - j); j = lps[j - 1]; }
            else if (i < n && text.charAt(i) != pattern.charAt(j)) {
                if (j > 0) j = lps[j - 1]; else i++;
            }
        }
        return result;
    }

    static java.util.List<Integer> bruteSearch(String text, String pattern) {
        java.util.List<Integer> result = new java.util.ArrayList<>();
        int n = text.length(), m = pattern.length();
        for (int i = 0; i <= n - m; i++) {
            int j = 0;
            while (j < m && text.charAt(i + j) == pattern.charAt(j)) j++;
            if (j == m) result.add(i);
        }
        return result;
    }
}

算法分析

预处理复杂度:坏字符表 O(m + |Σ|),好后缀表 O(m),其中 m 为模式串长度,Σ 为字符集。

搜索复杂度:最坏 O(n * m),但在实际文本(尤其是自然语言)中,平均接近 O(n/m),因为每次不匹配常能跳跃多个字符。在随机文本中,BM通常显著快于KMP。

空间复杂度:坏字符表 O(|Σ|),好后缀表 O(m)。