单模式串匹配算法中BM(Boyer-Moore)算法算是很难理解的算法了,不过性能高效,据说比KMP算法性能提升3到4倍,所以有必要学习下,才有可能在笔试或者面试中大显身手。
先看下BM算法原版简介
图片说明
图片说明
图片说明
图片说明
图片说明
图片说明

该算法从模式串的尾部开始匹配,且拥有在最坏情况下 O(N) 的时间复杂度。在算法介绍中,作者提出了好后缀规则(good-suffix shift)和坏字符规则(bad-character shift)。

坏字符规则(bad-character shift)

当文本串中的某个字符跟模式串的某个字符不匹配时,我们称文本串中的这个失配字符为坏字符,此时模式串需要向右移动,移动的位数 = 坏字符在模式串中的位置 - 坏字符在模式串中最右出现的位置。此外,如果"坏字符"不包含在模式串之中,则最右出现位置为 -1。坏字符针对的是文本串。

好后缀规则(good-suffix shift)

当字符失配时,后移位数 = 好后缀在模式串中的位置 - 好后缀在模式串上一次出现的位置,且如果好后缀在模式串中没有再次出现,则为 -1。好后缀针对的是模式串。

BM算法 的一个特点是当不匹配的时候 一次性可以跳过不止一个字符 。即它不需要对被搜索的字符串中的字符进行逐一比较,而会跳过其中某些部分。通常搜索关键字越长,算法速度越快。它的效率来自于这样的事实:对于每一次失败的匹配尝试,算法都能够使用这些信息来排除尽可能多的无法匹配的位置。

它充分利用待搜索字符串的 一些特征 ,加快了搜索的步骤。

那它是利用了什么特性去 排除尽可能多的无法匹配的位置 呢?

它是基于以下两个规则让模式串每次向右移动 尽可能大 的距离。

图片说明

坏字符规则

坏字符出现的时候有两种情况进行讨论。

1、模式串中没有出现了文本串中的那个坏字符,将模式串直接整体对齐到这个字符的后方,继续比较。
图片说明

图片说明

2、模式串中有对应的坏字符时,让模式串中 最靠右 的对应字符与坏字符相对。

这句话有一个关键词是 最靠右。

思考一下为什么是 最靠右?

看图!

图片说明

图片说明
图片说明
图片说明

好后缀规则

1、如果模式串中存在已经匹配成功的好后缀,则把目标串与好后缀对齐,然后从模式串的最尾元素开始往前匹配。
图片说明
图片说明
2、如果无法找到匹配好的后缀,找一个匹配的最长的前缀,让目标串与最长的前缀对齐(如果这个前缀存在的话)。模式串[m-s,m] = 模式串[0,s] 。
图片说明
图片说明

3、如果完全不存在和好后缀匹配的子串,则右移整个模式串。

GIF动图演示

图片说明

最终java版本的完整代码:

// a,b 表示主串和模式串;n,m 表示主串和模式串的长度。public int bm(char[] a, int n, char[] b, int m) {
  int[] bc = new int[SIZE]; // 记录模式串中每个字符最后出现的位置
  generateBC(b, m, bc); // 构建坏字符哈希表
  int[] suffix = new int[m];
  boolean[] prefix = new boolean[m];
  generateGS(b, m, suffix, prefix);
  int i = 0; // j 表示主串与模式串匹配的第一个字符
  while (i <= n - m) {
    int j;
    for (j = m - 1; j >= 0; --j) { // 模式串从后往前匹配
      if (a[i+j] != b[j]) break; // 坏字符对应模式串中的下标是 j
    }
    if (j < 0) {
      return i; // 匹配成功,返回主串与模式串第一个匹配的字符的位置
    }
    int x = j - bc[(int)a[i+j]];
    int y = 0;
    if (j < m-1) { // 如果有好后缀的话
      y = moveByGS(j, m, suffix, prefix);
    }
    i = i + Math.max(x, y);
  }
  return -1;
}

// j 表示坏字符对应的模式串中的字符下标 ; m 表示模式串长度
private int moveByGS(int j, int m, int[] suffix, boolean[] prefix) {
  int k = m - 1 - j; // 好后缀长度
  if (suffix[k] != -1) return j - suffix[k] +1;
  for (int r = j+2; r <= m-1; ++r) {
    if (prefix[m-r] == true) {
      return r;
    }
  }
  return m;
}