一文带你学会"Boyer-Moore算法"

大家好,我是算法学长,一个热爱分享算法知识的开发者。 算法 0 基础、校招冲刺中大厂,推荐使用算法导航:https://algo.codefather.cn/ 交互式算法学习平台,带大家系统化学习算法。

Boyer-Moore算法

介绍

Boyer-Moore算法是一种高效的字符串匹配算法,由 Robert S. Boyer和J Strother Moore 设计于1977年。它从右向左比较字符,并利用两个启发式规则(坏字符规则和好后缀规则)在不匹配情况下实现较大跳跃,减少比较次数。Boyer-Moore算法在实际应用中大部分情况下比朴素算法和KMP算法更高效

算法步骤

  1. 预处理模式串,构建坏字符表和好后缀表
  2. 将模式串对齐到文本串的开始位置
  3. 从模式串的最右侧字符开始比较,从右向左进行匹配
  4. 如果发生不匹配,通过以下规则计算跳转距离:
    • 坏字符规则:根据不匹配字符在模式串中的最右位置决定跳转距离
    • 好后缀规则:根据已匹配部分在模式串中的重复情况决定跳转距离
  5. 选择两个规则中的最大跳转距离,移动模式串
  6. 重复步骤3-5,直到找到匹配或到达文本串末尾

核心特性

  • 从右向左比较:与大多数字符串匹配算法不同,从模式串的末尾开始比较
  • 双规则跳转:利用坏字符规则和好后缀规则计算跳转距离
  • 时间复杂度:最坏情况O(m*n),m是模式串长度,n是文本串长度;平均情况接近O(n/m)
  • 空间复杂度:O(k+m),其中k是字符集大小,m是模式串长度
  • 适用范围:特别适合长模式串和大字符集场景

基础实现

接下来大家一起看下Boyer-Moore算法的部分主流语言实现:

Java实现

java
复制代码
public class BoyerMoore { private final int R; // 字符集大小 private int[] badChar; // 坏字符表 private int[] goodSuffix; // 好后缀表 private int[] borderPos; // 边界位置表 private String pattern; // 模式串 public BoyerMoore(String pattern) { this.R = 256; // ASCII字符集 this.pattern = pattern; int m = pattern.length(); // 初始化坏字符表 badChar = new int[R]; for (int c = 0; c < R; c++) { badChar[c] = -1; // 初始化为-1 } for (int j = 0; j < m; j++) { badChar[pattern.charAt(j)] = j; // 记录每个字符最右出现位置 } // 初始化好后缀表和边界位置表 goodSuffix = new int[m]; borderPos = new int[m]; processSuffixes(); } // 预处理好后缀表 private void processSuffixes() { int m = pattern.length(); int i = m, j = m + 1; borderPos[i] = j; // 计算边界位置 while (i > 0) { while (j <= m && pattern.charAt(i - 1) != pattern.charAt(j - 1)) { if (goodSuffix[j] == 0) { goodSuffix[j] = j - i; } j = borderPos[j]; } i--; j--; borderPos[i] = j; } // 计算好后缀表 j = borderPos[0]; for (i = 0; i <= m; i++) { if (goodSuffix[i] == 0) { goodSuffix[i] = j; } if (i == j) { j = borderPos[j]; } } } // 搜索文本串中的匹配 public int search(String text) { int n = text.length(); int m = pattern.length(); if (m == 0) return 0; int skip; for (int i = 0; i <= n - m; i += skip) { skip = 0; for (int j = m - 1; j >= 0; j--) { if (pattern.charAt(j) != text.charAt(i + j)) { // 坏字符规则 skip = Math.max(1, j - badChar[text.charAt(i + j)]); // 好后缀规则 if (j < m - 1) { skip = Math.max(skip, goodSuffix[j + 1]); } break; } } if (skip == 0) return i; // 找到匹配 } return -1; // 没有找到匹配 } // 测试 public static void main(String[] args) { String text = "HERE IS A SIMPLE EXAMPLE"; String pattern = "EXAMPLE"; BoyerMoore bm = new BoyerMoore(pattern); int position = bm.search(text); if (position == -1) { System.out.println("未找到匹配"); } else { System.out.println("模式串在位置 " + position + " 处匹配"); System.out.println(text); for (int i = 0; i < position; i++) { System.out.print(" "); } System.out.println(pattern); } } }

JavaScript 实现

javascript
复制代码
class BoyerMoore { constructor(pattern) { this.pattern = pattern; this.R = 256; // ASCII字符集 const m = pattern.length; // 初始化坏字符表 this.badChar = new Array(this.R).fill(-1); for (let j = 0; j < m; j++) { this.badChar[pattern.charCodeAt(j)] = j; } // 初始化好后缀表和边界位置表 this.goodSuffix = new Array(m + 1).fill(0); this.borderPos = new Array(m + 1).fill(0); this.processSuffixes(); } // 预处理好后缀表 processSuffixes() { const m = this.pattern.length; let i = m, j = m + 1; this.borderPos[i] = j; // 计算边界位置 while (i > 0) { while (j <= m && this.pattern.charAt(i - 1) !== this.pattern.charAt(j - 1)) { if (this.goodSuffix[j] === 0) { this.goodSuffix[j] = j - i; } j = this.borderPos[j]; } i--; j--; this.borderPos[i] = j; } // 计算好后缀表 j = this.borderPos[0]; for (i = 0; i <= m; i++) { if (this.goodSuffix[i] === 0) { this.goodSuffix[i] = j; } if (i === j) { j = this.borderPos[j]; } } } // 搜索文本串中的匹配 search(text) { const n = text.length; const m = this.pattern.length; if (m === 0) return 0; let skip; for (let i = 0; i <= n - m; i += skip) { skip = 0; for (let j = m - 1; j >= 0; j--) { if (this.pattern.charAt(j) !== text.charAt(i + j)) { // 坏字符规则 skip = Math.max(1, j - this.badChar[text.charCodeAt(i + j)]); // 好后缀规则 if (j < m - 1) { skip = Math.max(skip, this.goodSuffix[j + 1]); } break; } } if (skip === 0) return i; // 找到匹配 } return -1; // 没有找到匹配 } } // 测试 const text = "HERE IS A SIMPLE EXAMPLE"; const pattern = "EXAMPLE"; const bm = new BoyerMoore(pattern); const position = bm.search(text); if (position === -1) { console.log("未找到匹配"); } else { console.log(`模式串在位置 ${position} 处匹配`); console.log(text); console.log(" ".repeat(position) + pattern); }

Python 实现

python
复制代码
class BoyerMoore: def __init__(self, pattern): self.pattern = pattern self.R = 256 # ASCII字符集 m = len(pattern) # 初始化坏字符表 self.bad_char = [-1] * self.R for j in range(m): self.bad_char[ord(pattern[j])] = j # 初始化好后缀表和边界位置表 self.good_suffix = [0] * (m + 1) self.border_pos = [0] * (m + 1) self.process_suffixes() # 预处理好后缀表 def process_suffixes(self): m = len(self.pattern) i, j = m, m + 1 self.border_pos[i] = j # 计算边界位置 while i > 0: while j <= m and self.pattern[i - 1] != self.pattern[j - 1]: if self.good_suffix[j] == 0: self.good_suffix[j] = j - i j = self.border_pos[j] i -= 1 j -= 1 self.border_pos[i] = j # 计算好后缀表 j = self.border_pos[0] for i in range(m + 1): if self.good_suffix[i] == 0: self.good_suffix[i] = j if i == j: j = self.border_pos[j] # 搜索文本串中的匹配 def search(self, text): n = len(text) m = len(self.pattern) if m == 0: return 0 i = 0 while i <= n - m: j = m - 1 while j >= 0 and self.pattern[j] == text[i + j]: j -= 1 if j < 0: return i # 找到匹配 # 坏字符规则 bc_skip = max(1, j - self.bad_char[ord(text[i + j])]) # 好后缀规则 gs_skip = 0 if j < m - 1: gs_skip = self.good_suffix[j + 1] i += max(bc_skip, gs_skip) return -1 # 没有找到匹配 # 测试 text = "HERE IS A SIMPLE EXAMPLE" pattern = "EXAMPLE" bm = BoyerMoore(pattern) position = bm.search(text) if position == -1: print("未找到匹配") else: print(f"模式串在位置 {position} 处匹配") print(text) print(" " * position + pattern)

Go 实现

go
复制代码
package main import ( "fmt" "math" ) type BoyerMoore struct { pattern string badChar []int goodSuffix []int borderPos []int } func NewBoyerMoore(pattern string) *BoyerMoore { R := 256 // ASCII字符集 m := len(pattern) // 初始化坏字符表 badChar := make([]int, R) for i := 0; i < R; i++ { badChar[i] = -1 } for j := 0; j < m; j++ { badChar[pattern[j]] = j } // 初始化好后缀表和边界位置表 goodSuffix := make([]int, m+1) borderPos := make([]int, m+1) bm := &BoyerMoore{ pattern: pattern, badChar: badChar, goodSuffix: goodSuffix, borderPos: borderPos, } bm.processSuffixes() return bm } // 预处理好后缀表 func (bm *BoyerMoore) processSuffixes() { m := len(bm.pattern) i, j := m, m+1 bm.borderPos[i] = j // 计算边界位置 for i > 0 { for j <= m && bm.pattern[i-1] != bm.pattern[j-1] { if bm.goodSuffix[j] == 0 { bm.goodSuffix[j] = j - i } j = bm.borderPos[j] } i-- j-- bm.borderPos[i] = j } // 计算好后缀表 j = bm.borderPos[0] for i := 0; i <= m; i++ { if bm.goodSuffix[i] == 0 { bm.goodSuffix[i] = j } if i == j { j = bm.borderPos[j] } } } // 搜索文本串中的匹配 func (bm *BoyerMoore) Search(text string) int { n := len(text) m := len(bm.pattern) if m == 0 { return 0 } i := 0 for i <= n-m { j := m - 1 for j >= 0 && bm.pattern[j] == text[i+j] { j-- } if j < 0 { return i // 找到匹配 } // 坏字符规则 bcSkip := math.Max(1, float64(j-bm.badChar[text[i+j]])) // 好后缀规则 gsSkip := 0 if j < m-1 { gsSkip = bm.goodSuffix[j+1] } i += int(math.Max(bcSkip, float64(gsSkip))) } return -1 // 没有找到匹配 } func main() { text := "HERE IS A SIMPLE EXAMPLE" pattern := "EXAMPLE" bm := NewBoyerMoore(pattern) position := bm.Search(text) if position == -1 { fmt.Println("未找到匹配") } else { fmt.Printf("模式串在位置 %d 处匹配\n", position) fmt.Println(text) for i := 0; i < position; i++ { fmt.Print(" ") } fmt.Println(pattern) } }

C 实现

c
复制代码
#include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX_CHAR 256 // 预处理坏字符表 void precomputeBadChar(char *pattern, int m, int badChar[]) { int i; // 初始化所有字符为-1 for (i = 0; i < MAX_CHAR; i++) { badChar[i] = -1; } // 记录每个字符最右出现的位置 for (i = 0; i < m; i++) { badChar[(unsigned char)pattern[i]] = i; } } // 预处理好后缀表和边界位置表 void precomputeGoodSuffix(char *pattern, int m, int goodSuffix[], int borderPos[]) { int i = m, j = m + 1; borderPos[i] = j; // 计算边界位置 while (i > 0) { while (j <= m && pattern[i - 1] != pattern[j - 1]) { if (goodSuffix[j] == 0) { goodSuffix[j] = j - i; } j = borderPos[j]; } i--; j--; borderPos[i] = j; } // 计算好后缀表 j = borderPos[0]; for (i = 0; i <= m; i++) { if (goodSuffix[i] == 0) { goodSuffix[i] = j; } if (i == j) { j = borderPos[j]; } } } // Boyer-Moore搜索算法 int boyerMooreSearch(char *text, char *pattern) { int n = strlen(text); int m = strlen(pattern); if (m == 0) return 0; if (n < m) return -1; // 预处理 int badChar[MAX_CHAR]; int *goodSuffix = (int*)calloc(m + 1, sizeof(int)); int *borderPos = (int*)calloc(m + 1, sizeof(int)); precomputeBadChar(pattern, m, badChar); precomputeGoodSuffix(pattern, m, goodSuffix, borderPos); // 搜索 int i = 0, j, skip; while (i <= n - m) { j = m - 1; // 从右向左匹配 while (j >= 0 && pattern[j] == text[i + j]) { j--; } if (j < 0) { // 找到匹配 free(goodSuffix); free(borderPos); return i; } // 计算坏字符规则的跳转距离 int bcSkip = j - badChar[(unsigned char)text[i + j]]; // 计算好后缀规则的跳转距离 int gsSkip = 0; if (j < m - 1) { gsSkip = goodSuffix[j + 1]; } // 取两者中的最大值 skip = bcSkip > gsSkip ? bcSkip : gsSkip; skip = skip > 1 ? skip : 1; i += skip; } free(goodSuffix); free(borderPos); return -1; // 未找到匹配 } int main() { char text[] = "HERE IS A SIMPLE EXAMPLE"; char pattern[] = "EXAMPLE"; int position = boyerMooreSearch(text, pattern); if (position == -1) { printf("未找到匹配\n"); } else { printf("模式串在位置 %d 处匹配\n", position); printf("%s\n", text); // 打印指示匹配位置的指针 for (int i = 0; i < position; i++) { printf(" "); } printf("%s\n", pattern); } return 0; }

C++实现

cpp
复制代码
#include <iostream> #include <string> #include <vector> #include <algorithm> class BoyerMoore { private: std::string pattern; std::vector<int> badChar; std::vector<int> goodSuffix; std::vector<int> borderPos; const int R = 256; // ASCII字符集 // 预处理好后缀表 void processSuffixes() { int m = pattern.length(); int i = m, j = m + 1; borderPos[i] = j; // 计算边界位置 while (i > 0) { while (j <= m && pattern[i - 1] != pattern[j - 1]) { if (goodSuffix[j] == 0) { goodSuffix[j] = j - i; } j = borderPos[j]; } i--; j--; borderPos[i] = j; } // 计算好后缀表 j = borderPos[0]; for (i = 0; i <= m; i++) { if (goodSuffix[i] == 0) { goodSuffix[i] = j; } if (i == j) { j = borderPos[j]; } } } public: BoyerMoore(const std::string& pat) : pattern(pat) { int m = pattern.length(); // 初始化坏字符表 badChar.resize(R, -1); for (int j = 0; j < m; j++) { badChar[static_cast<unsigned char>(pattern[j])] = j; } // 初始化好后缀表和边界位置表 goodSuffix.resize(m + 1, 0); borderPos.resize(m + 1, 0); processSuffixes(); } // 搜索文本串中的匹配 int search(const std::string& text) { int n = text.length(); int m = pattern.length(); if (m == 0) return 0; if (n < m) return -1; int i = 0; while (i <= n - m) { int j = m - 1; // 从右向左匹配 while (j >= 0 && pattern[j] == text[i + j]) { j--; } if (j < 0) { return i; // 找到匹配 } // 计算坏字符规则的跳转距离 int bcSkip = j - badChar[static_cast<unsigned char>(text[i + j])]; // 计算好后缀规则的跳转距离 int gsSkip = 0; if (j < m - 1) { gsSkip = goodSuffix[j + 1]; } // 取两者中的最大值 i += std::max(1, std::max(bcSkip, gsSkip)); } return -1; // 未找到匹配 } }; int main() { std::string text = "HERE IS A SIMPLE EXAMPLE"; std::string pattern = "EXAMPLE"; BoyerMoore bm(pattern); int position = bm.search(text); if (position == -1) { std::cout << "未找到匹配" << std::endl; } else { std::cout << "模式串在位置 " << position << " 处匹配" << std::endl; std::cout << text << std::endl; // 打印指示匹配位置的指针 for (int i = 0; i < position; i++) { std::cout << " "; } std::cout << pattern << std::endl; } return 0; }

优化策略

简化好后缀表构建

对于一些应用场景,可以只使用坏字符规则,简化算法实现:

java
复制代码
public class SimplifiedBoyerMoore { private final int R; // 字符集大小 private int[] badChar; // 坏字符表 private String pattern; // 模式串 public SimplifiedBoyerMoore(String pattern) { this.R = 256; // ASCII字符集 this.pattern = pattern; int m = pattern.length(); // 初始化坏字符表 badChar = new int[R]; for (int c = 0; c < R; c++) { badChar[c] = -1; // 初始化为-1 } for (int j = 0; j < m; j++) { badChar[pattern.charAt(j)] = j; // 记录每个字符最右出现位置 } } // 搜索文本串中的匹配 public int search(String text) { int n = text.length(); int m = pattern.length(); if (m == 0) return 0; int skip; for (int i = 0; i <= n - m; i += skip) { skip = 0; for (int j = m - 1; j >= 0; j--) { if (pattern.charAt(j) != text.charAt(i + j)) { // 仅使用坏字符规则 skip = Math.max(1, j - badChar[text.charAt(i + j)]); break; } } if (skip == 0) return i; // 找到匹配 } return -1; // 没有找到匹配 } }

缓存预计算结果

针对需要重复搜索同一模式串的场景,可以预计算并缓存结果:

java
复制代码
public class CachedBoyerMoore { private Map<String, BoyerMoore> cache = new HashMap<>(); public int search(String text, String pattern) { // 检查缓存中是否有预计算的Boyer-Moore对象 BoyerMoore bm = cache.get(pattern); if (bm == null) { bm = new BoyerMoore(pattern); cache.put(pattern, bm); } return bm.search(text); } }

优缺点

优点

  • 在实际应用中,大部分场景比KMP和朴素算法更高效
  • 最好情况下可以跳过大量文本,实现亚线性时间复杂度
  • 对于长模式串和大字符集特别有效
  • 预处理跟模式串有关,与文本串长度无关

缺点

  • 预处理复杂,特别是好后缀表的构建
  • 需要额外空间存储坏字符表和好后缀表
  • 最坏情况下时间复杂度仍为O(m*n)
  • 对于短模式串,预处理开销可能抵消算法优势
  • 好后缀规则的实现较复杂,容易出错

应用场景

1)文本编辑器的查找功能

2)网络安全中的特征码匹配

3)自然语言处理中的关键词检索

4)大规模文本数据处理

扩展

Horspool算法

Horspool算法是Boyer-Moore的简化版本,只使用坏字符规则,但是对坏字符表进行了修改:

java
复制代码
public class Horspool { private final int R; // 字符集大小 private int[] badChar; // 坏字符表 private String pattern; // 模式串 public Horspool(String pattern) { this.R = 256; // ASCII字符集 this.pattern = pattern; int m = pattern.length(); // 初始化坏字符表 badChar = new int[R]; // 所有字符默认移动模式串长度 for (int c = 0; c < R; c++) { badChar[c] = m; } // 模式串中的字符(除了最后一个)设置为对应值 for (int j = 0; j < m - 1; j++) { badChar[pattern.charAt(j)] = m - 1 - j; } } // 搜索文本串中的匹配 public int search(String text) { int n = text.length(); int m = pattern.length(); if (m == 0) return 0; if (m > n) return -1; int i = m - 1; // 从模式串最后一个字符对齐开始 while (i < n) { int k = 0; while (k < m && pattern.charAt(m - 1 - k) == text.charAt(i - k)) { k++; } if (k == m) { return i - m + 1; // 找到匹配 } // 使用坏字符规则移动 i += badChar[text.charAt(i)]; } return -1; // 没有找到匹配 } }

Sunday算法

Sunday算法是另一种Boyer-Moore的变种,它关注的是文本串中模式串后面的字符:

java
复制代码
public class Sunday { private final int R; // 字符集大小 private int[] shift; // 移动表 private String pattern; // 模式串 public Sunday(String pattern) { this.R = 256; // ASCII字符集 this.pattern = pattern; int m = pattern.length(); // 初始化移动表 shift = new int[R]; // 所有字符默认移动模式串长度+1 for (int c = 0; c < R; c++) { shift[c] = m + 1; } // 模式串中的字符设置为对应值 for (int j = 0; j < m; j++) { shift[pattern.charAt(j)] = m - j; } } // 搜索文本串中的匹配 public int search(String text) { int n = text.length(); int m = pattern.length(); if (m == 0) return 0; if (m > n) return -1; int i = 0; // 从文本串开始位置 while (i <= n - m) { int j = 0; while (j < m && pattern.charAt(j) == text.charAt(i + j)) { j++; } if (j == m) { return i; // 找到匹配 } // 下一个位置超出文本串长度,返回-1 if (i + m >= n) { return -1; } // 使用Sunday算法的移动规则 i += shift[text.charAt(i + m)]; } return -1; // 没有找到匹配 } }

测验

这里准备了一些测试题,方便大家判断自己的掌握情况:

  1. Boyer-Moore算法的两个主要启发式规则是什么?
  2. 为什么Boyer-Moore算法从右向左比较字符?
  3. 在最好情况下,Boyer-Moore算法的时间复杂度是多少?
  4. 坏字符规则和好后缀规则如何协同工作?

测验答案

  1. 坏字符规则和好后缀规则。
  2. 从右向左比较可以更快发现不匹配,然后通过坏字符和好后缀规则计算更大的跳转距离。
  3. 最好情况下Boyer-Moore算法的时间复杂度可以达到O(n/m)。
  4. 分别计算坏字符规则和好后缀规则跳转距离,然后选择两者中的最大值作为实际跳转距离,保证安全、高效的跳转。

相关的 LeetCode 热门题目

给大家推荐一些可以用来练手的 LeetCode 题目:

学算法就上算法导航:https://algo.codefather.cn/ ,交互式算法学习平台!

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP