引言
Boyer-Moore(BM)算法是字符串匹配领域中最快的实用算法之一。与从左向右逐字符比较的朴素方法不同,BM算法从模式串的末尾开始比较,并利用预处理的坏字符规则(Bad Character Rule)和好后缀规则(Good Suffix Rule)实现大步长跳跃。在最优情况下,BM算法的时间复杂度可达O(n/m),其中n为文本长度,m为模式串长度。本文将用Java完整实现BM算法,包括两种启发式规则的预处理与联合决策逻辑。
核心思想
假设文本为T,模式串为P,从位置i开始对齐:
- 从右向左比较:先比较P[m-1]与T[i+m-1],若匹配则继续向左
- 坏字符规则:当T[i+j] ≠ P[j]时,若T[i+j]在P中出现过,则将P中该字符最右出现位置对齐到T[i+j];若未出现,则直接将P移到该字符右侧
- 好后缀规则:当已匹配后缀P[j+1..m-1] = T[i+j+1..i+m-1]但P[j] ≠ T[i+j]时,查找P中是否有相同后缀可以重新对齐
实际位移取两种规则计算值的最大值,确保安全跳过不可能匹配的位置。
Java实现
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* Boyer-Moore字符串匹配算法完整实现
* 包含坏字符规则与好后缀规则
*/
public class BoyerMooreSearch {
private final String pattern;
private final int m; // 模式串长度
private final Map<Character, Integer> badCharTable; // 坏字符表
private final int[] goodSuffixTable; // 好后缀表
/**
* 构造函数:预处理模式串
*/
public BoyerMooreSearch(String pattern) {
this.pattern = pattern;
this.m = pattern.length();
this.badCharTable = buildBadCharTable();
this.goodSuffixTable = buildGoodSuffixTable();
}
/**
* 构建坏字符表
* 记录每个字符在模式串中最右出现的位置(从0开始)
* 若字符不在模式串中,默认为-1
*/
private Map<Character, Integer> buildBadCharTable() {
Map<Character, Integer> table = new HashMap<>();
for (int i = 0; i < m; i++) {
table.put(pattern.charAt(i), i); // 后出现的覆盖先出现的,保留最右位置
}
return table;
}
/**
* 查询坏字符规则给出的位移量
* @param badChar 文本中不匹配的字符
* @param badPos 该字符在模式串中的位置(从0开始)
* @return 模式串应向右移动的位数
*/
private int getBadCharShift(char badChar, int badPos) {
Integer rightmostPos = badCharTable.get(badChar);
if (rightmostPos == null) {
// 字符不在模式串中,直接将模式串移到该字符右侧
return badPos + 1;
}
// 最右出现位置在badPos左侧:直接对齐
// 最右出现位置在badPos右侧或等于:避免回溯,移动1位
int shift = badPos - rightmostPos;
return shift > 0 ? shift : 1;
}
/**
* 构建好后缀表
* goodSuffixTable[i] 表示当不匹配发生在位置i时(即P[0..i-1]已匹配),
* 模式串需要向右移动的距离
*
* 核心逻辑:
* 1. 情况A:P中存在子串与好后缀匹配,将最右侧的匹配子串对齐
* 2. 情况B:P的前缀与好后缀的后缀匹配,将前缀对齐
* 3. 情况C:都不满足,移动m位
*/
private int[] buildGoodSuffixTable() {
int[] table = new int[m];
int[] suffix = computeSuffixLengths(); // suffix[i] = 以i结尾的最长匹配后缀长度
// 初始化:情况C,全部设为m
Arrays.fill(table, m);
// 情况B:处理P的前缀与好后缀后缀匹配的情况
// 从右向左扫描,找到前缀匹配
int j = 0;
for (int i = m - 1; i >= 0; i--) {
if (suffix[i] == i + 1) { // P[0..i]是某个后缀的前缀匹配
for (; j < m - 1 - i; j++) {
if (table[j] == m) {
table[j] = m - 1 - i;
}
}
}
}
// 情况A:处理内部子串匹配
for (int i = 0; i < m - 1; i++) {
table[m - 1 - suffix[i]] = m - 1 - i;
}
return table;
}
/**
* 计算suffix数组
* suffix[i] = 满足 P[i-s+1..i] == P[m-s..m-1] 的最大s值
* 即:以位置i结尾的子串,与模式串后缀匹配的最大长度
*/
private int[] computeSuffixLengths() {
int[] suffix = new int[m];
suffix[m - 1] = m; // 最后一个字符,整个串都是后缀
int g = m - 1; // 上一次失配的位置
int f = 0; // 当前匹配子串的起始位置
for (int i = m - 2; i >= 0; i--) {
if (i > g && suffix[i + m - 1 - f] < i - g) {
// 利用之前计算的结果
suffix[i] = suffix[i + m - 1 - f];
} else {
// 直接扩展比较
if (i < g) g = i;
f = i;
while (g >= 0 && pattern.charAt(g) == pattern.charAt(g + m - 1 - f)) {
g--;
}
suffix[i] = f - g;
}
}
return suffix;
}
/**
* 查询好后缀规则给出的位移量
* @param badPos 不匹配发生的位置
* @return 位移量
*/
private int getGoodSuffixShift(int badPos) {
return goodSuffixTable[badPos];
}
/**
* 在文本中搜索模式串,返回首次匹配的位置(从0开始),未找到返回-1
*/
public int search(String text) {
int n = text.length();
if (m == 0) return 0;
if (m > n) return -1;
int i = 0; // 模式串在文本中的起始位置
while (i <= n - m) {
int j = m - 1; // 从模式串末尾开始比较
// 从右向左匹配
while (j >= 0 && pattern.charAt(j) == text.charAt(i + j)) {
j--;
}
if (j < 0) {
// 完全匹配
return i;
}
// 失配发生在位置j
char badChar = text.charAt(i + j);
int badCharShift = getBadCharShift(badChar, j);
int goodSuffixShift = getGoodSuffixShift(j);
// 取两种规则的最大值(更安全的跳跃)
int shift = Math.max(badCharShift, goodSuffixShift);
i += shift;
}
return -1;
}
/**
* 在文本中搜索所有匹配位置
*/
public java.util.List<Integer> searchAll(String text) {
java.util.List<Integer> results = new java.util.ArrayList<>();
int n = text.length();
if (m == 0 || m > n) return results;
int i = 0;
while (i <= n - m) {
int j = m - 1;
while (j >= 0 && pattern.charAt(j) == text.charAt(i + j)) {
j--;
}
if (j < 0) {
results.add(i);
// 继续搜索:使用好后缀规则移动
i += goodSuffixTable[0];
} else {
char badChar = text.charAt(i + j);
int shift = Math.max(getBadCharShift(badChar, j), getGoodSuffixShift(j));
i += shift;
}
}
return results;
}
/**
* 打印预处理表(调试用)
*/
public void printTables() {
System.out.println("模式串: " + pattern);
System.out.println("坏字符表:");
for (Map.Entry<Character, Integer> entry : badCharTable.entrySet()) {
System.out.printf(" '%c' -> %d%n", entry.getKey(), entry.getValue());
}
System.out.println("好后缀表 (位置->位移):");
for (int i = 0; i < m; i++) {
System.out.printf(" 失配在P[%d]: 移动%d%n", i, goodSuffixTable[i]);
}
}
/**
* 与朴素算法对比性能
*/
public static void benchmark() {
// 构造大文本
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) {
sb.append("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
}
sb.append("PATTERN_MATCH_END");
String text = sb.toString();
String pattern = "PATTERN_MATCH_END";
// BM算法
BoyerMooreSearch bm = new BoyerMooreSearch(pattern);
long start = System.nanoTime();
int bmResult = bm.search(text);
long bmTime = System.nanoTime() - start;
// 朴素算法
start = System.nanoTime();
int naiveResult = text.indexOf(pattern);
long naiveTime = System.nanoTime() - start;
System.out.printf("文本长度: %d, 模式串长度: %d%n", text.length(), pattern.length());
System.out.printf("BM算法: 结果=%d, 耗时=%.3fms%n", bmResult, bmTime / 1e6);
System.out.printf("朴素算法: 结果=%d, 耗时=%.3fms%n", naiveResult, naiveTime / 1e6);
System.out.printf("加速比: %.1fx%n", (double) naiveTime / bmTime);
}
public static void main(String[] args) {
// 基础功能演示
String text = "ABAAABCDABCABCDABCDABDE";
String pattern = "ABCABCD";
BoyerMooreSearch bm = new BoyerMooreSearch(pattern);
bm.printTables();
int pos = bm.search(text);
System.out.printf("%n在\"%s\"中搜索\"%s\": 首次匹配位置=%d%n", text, pattern, pos);
java.util.List<Integer> allPos = bm.searchAll(text);
System.out.println("所有匹配位置: " + allPos);
// 性能测试
System.out.println("\n=== 性能对比 ===");
benchmark();
}
}
复杂度分析
- 预处理:
- 坏字符表:O(m × |Σ|) 时间,O(|Σ|) 空间(|Σ|为字符集大小)
- 好后缀表:O(m) 时间,O(m) 空间
- 搜索:
- 最坏情况:O(nm)(如文本为”AAAAA”,模式为”AAAB”)
- 平均情况:O(n/m) 次比较,实际运行极快
- 最优情况:O(n/m) 当字符集较大时
算法变体与选择
| 算法 | 预处理 | 搜索最优 | 搜索最坏 | 适用场景 |
|---|---|---|---|---|
| 朴素 | O(1) | O(n) | O(nm) | 极短模式串 |
| KMP | O(m) | O(n) | O(n) | 需要稳定保证 |
| BM | O(m+|Σ|) | O(n/m) | O(nm) | 大字符集、长模式串 |
| Sunday | O(m+|Σ|) | O(n/m) | O(nm) | 实现更简单 |
Boyer-Moore算法是grep等文本搜索工具的核心基础,理解其双启发式规则的设计思想,对掌握字符串算法的高阶技巧至关重要。