Java:对给定的字符串和给定的模式执行Boyer-Moore搜索算法(附带源码)

一、项目背景详细介绍

在文本处理与信息检索中,需要在海量文本中高效地查找模式串(Pattern)。经典的朴素搜素在最坏情况下时间复杂度为 O(N·M),效率不够高。Boyer–Moore 算法则采用“坏字符”与“好后缀”两种启发规则,从模式尾部匹配开始,通常能大幅跳过不可能匹配的位置,平均时间复杂度接近 O(N/M),在实际应用(如 grep、数据库索引)中非常高效。

本项目旨在用 Java 实现 Boyer–Moore 搜索算法,能够针对任意给定的文本串和模式串,快速定位所有匹配的位置,并返回其索引列表。


二、项目需求详细介绍

  1. 功能需求

    • 提供静态方法

public static List boyerMooreSearch(String text, String pattern)
      • 输入:

        • text:待搜索文本,长度 N;

        • pattern:模式串,长度 M;

      • 输出:返回所有匹配起始下标的 List;若无匹配则返回空列表。

  1. 性能需求

    • 构建预处理表(坏字符和好后缀)时间 O(M + Σ);

    • 搜索时间期望 O(N/M) 至 O(N + M);最坏 O(N·M)(极端重复字符时)。

  2. 可扩展需求

    • 支持 Unicode 字符集(用 Map 或直接基于 char 的表);

    • 支持一次性多模式匹配(可扩展为 Aho–Corasick);

    • 支持忽略大小写或正则方式的匹配。

  3. 质量需求

    • 代码注释清晰;

    • 单元测试覆盖常见与边界场景;

    • 无外部依赖,仅用 JDK 标准库;


三、相关技术详细介绍

  1. 坏字符规则(Bad Character Rule)

    • 对模式串从左到右构建一个最后出现位置表 badChar[char] = index

    • 当 text[i+j] ≠ pattern[j] 时,可将模式右移 j - badChar[text[i+j]]

  2. 好后缀规则(Good Suffix Rule)

    • 找到模式中已成功匹配的后缀,在其前面再出现的位置;

    • 构造两个辅助数组 suffix[]prefix[] 来计算最大位移;

  3. 组合两种规则

    • 在匹配失败时,取坏字符和好后缀跳转量的最大值,保证不漏匹配且尽可能多地跳过无效位置。


四、实现思路详细介绍

  1. 预处理坏字符表

    • 数组大小可定为 256(针对 ASCII)或使用 Map 支持 Unicode;

    • 初始化为 -1,然后遍历模式串更新。

  2. 预处理好后缀表

    • 构造 suffix[k]:表示长度为 k 的后缀子串在模式中最左侧的位置;

    • 构造 prefix[k]:长度为 k 的后缀是否为模式的前缀;

    • 遍历模式串的每个 i,枚举 j=i+1 到 M-1,更新后缀长度。

  3. 搜索过程

    • i = 0;当 i ≤ N - M 时:

      • 从后向前比较 j 从 M−1 到 0;

      • 若匹配失败于 j,则计算:

        • 坏字符位移:j - badChar[text[i+j]]

        • 好后缀位移:调用 moveByGoodSuffix(j, M, suffix, prefix)

        • 取两者最大值 shift,执行 i += shift

      • 若 j < 0,表明完全匹配,将 i 加入结果,i += M(或下一个规则)。


五、完整实现代码

// 文件:src/main/java/com/example/search/BoyerMoore.java
package com.example.search;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Boyer–Moore 字符串搜索算法
 */
public class BoyerMoore {

    /**
     * 主搜索方法
     * @param text    待搜索文本
     * @param pattern 模式串
     * @return 匹配位置列表
     */
    public static List boyerMooreSearch(String text, String pattern) {
        List result = new ArrayList<>();
        if (text == null || pattern == null || pattern.length() == 0 || text.length() < pattern.length()) {
            return result;
        }
        char[] txt = text.toCharArray();
        char[] pat = pattern.toCharArray();
        int n = txt.length, m = pat.length;

        // 1. 坏字符预处理
        int[] badChar = new int[256];
        Arrays.fill(badChar, -1);
        for (int i = 0; i < m; i++) {
            badChar[pat[i]] = i;
        }

        // 2. 好后缀预处理
        int[] suffix = new int[m];
        boolean[] prefix = new boolean[m];
        Arrays.fill(suffix, -1);
        Arrays.fill(prefix, false);
        for (int i = 0; i < m - 1; i++) {
            int j = i, k = 0;
            // 从 pat[0..i] 中匹配 pat[i+1..m-1] 的后缀
            while (j >= 0 && pat[j] == pat[m - 1 - k]) {
                j--;
                k++;
                suffix[k] = j + 1;
            }
            if (j == -1) {
                prefix[k] = true;
            }
        }

        // 3. 搜索
        int i = 0;
        while (i <= n - m) {
            int j;
            for (j = m - 1; j >= 0; j--) {
                if (txt[i + j] != pat[j]) break;
            }
            if (j < 0) {
                result.add(i);
                i += m;  // 完全匹配,跳过整个模式串
            } else {
                int badShift = j - badChar[txt[i + j]];
                int goodShift = 0;
                if (j < m - 1) {
                    goodShift = moveByGoodSuffix(j, m, suffix, prefix);
                }
                i += Math.max(badShift, goodShift);
            }
        }
        return result;
    }

    /**
     * 计算好后缀规则的位移距离
     * @param j       首次失配下标
     * @param m       模式串长度
     * @param suffix  后缀数组
     * @param prefix  前缀标记数组
     * @return 位移距离
     */
    private static int moveByGoodSuffix(int j, int m, int[] suffix, boolean[] prefix) {
        int k = m - 1 - j;  // 已匹配的后缀长度
        // 1. 在模式中寻找与后缀 pat[j+1..m-1] 相同的子串出现位置
        if (suffix[k] != -1) {
            return j + 1 - suffix[k];
        }
        // 2. 否则,寻找最大的 r,使得 pat[0..r-1] = 后缀 pat[m-r..m-1]
        for (int r = j + 2; r <= m - 1; r++) {
            if (prefix[m - r]) {
                return r;
            }
        }
        // 3. 均不满足,则整体右移 m
        return m;
    }

    // 示例 main
    public static void main(String[] args) {
        String text = "HERE IS A SIMPLE EXAMPLE";
        String pattern = "EXAMPLE";
        List matches = boyerMooreSearch(text, pattern);
        System.out.println("匹配位置:" + matches);
    }
}
// 文件:src/test/java/com/example/search/BoyerMooreTest.java
package com.example.search;

import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;

/**
 * 单元测试:Boyer–Moore 搜索
 */
public class BoyerMooreTest {

    @Test
    public void testSingleMatch() {
        List res = BoyerMoore.boyerMooreSearch("ABC ABCDAB ABCDABCDABDE", "ABCDABD");
        assertEquals(List.of(15), res);
    }

    @Test
    public void testMultipleMatches() {
        List res = BoyerMoore.boyerMooreSearch("TESTTESTTEST", "TEST");
        assertEquals(List.of(0, 4, 8), res);
    }

    @Test
    public void testNoMatch() {
        List res = BoyerMoore.boyerMooreSearch("HELLO WORLD", "TEST");
        assertTrue(res.isEmpty());
    }

    @Test
    public void testPatternLongerThanText() {
        assertTrue(BoyerMoore.boyerMooreSearch("SHORT", "LONGPATTERN").isEmpty());
    }

    @Test
    public void testEmptyPatternOrText() {
        assertTrue(BoyerMoore.boyerMooreSearch("", "A").isEmpty());
        assertTrue(BoyerMoore.boyerMooreSearch("ABC", "").isEmpty());
    }
}

 

六、代码详细解读

  • 坏字符预处理
    使用长度 256 的数组记录每个字符在模式中最后出现的位置,匹配失败时即可 O(1) 计算跳转量。

  • 好后缀预处理
    通过 suffixprefix 两个辅助数组,分别标记模式中可重用的后缀起始位置及是否与模式前缀匹配,用于计算更大跳转。

  • 搜索主循环
    内层从模式尾部向前比对,若完全匹配,记录当前位置并跳过整个模式;若失配,分别计算坏字符与好后缀位移,取最大值跳转。

  • moveByGoodSuffix 方法
    针对已匹配的后缀长度 k,先看是否存在同长度的内部子串可对齐;否则查找最长前缀能与后缀对齐;都不行则全长跳转。


七、项目详细总结

本实现完整覆盖 Boyer–Moore 算法的两大核心规则——坏字符与好后缀,兼顾了字符集预处理与位移计算。平均情况下,移位距离较大,搜索效率远超朴素算法,适合大文本高频匹配场景。


八、项目常见问题及解答

  1. 问:字符集较大如何优化坏字符表?
    答:可使用 Map 或根据实际字符范围调整数组大小。

  2. 问:模式串包含 Unicode 时如何处理?
    答:同理,使用 int[] 大表或 Map,并注意 Java char 为 UTF-16 单元。

  3. 问:好后缀预处理复杂度高吗?
    答:预处理为 O(M²),但 M 通常较短;可优化为 O(M) 的 Z 算法或 N log N 方案。

  4. 问:如何扩展到多模式匹配?
    答:可考虑 Aho–Corasick 算法,将多模式构建成 Trie,并配合失败指针进行高效搜索。


九、扩展方向与性能优化

  1. 优化好后缀为 O(M):利用前缀函数或 Z 算法加速预处理;

  2. 并行流搜索:对超大文本分块并行匹配,然后合并结果;

  3. 增量搜索:支持滚动窗口或流式输入,无需一次性加载全文本;

  4. GPU 加速:在海量文本下,可将相似子串匹配映射到 GPU 并行比对;

  5. 混合算法:结合 Horspool 简化版本或 Sunday 算法,在不同场景下自适切换。

你可能感兴趣的:(Java算法完整教程,java,开发语言)