介绍:BM 算法是一种高效的字符串匹配算法,名称由两个发明者姓名的首字母组成。该算法有两类规则:坏字符规则和好后缀规则,其中好后缀规则可以独立于坏字符规则使用,在内存要求比较严格时,可以只使用好后缀规则来实现。BM 算法的时间复杂度分析非常复杂,有数据表明,在实践中 BM 算法比 KMP 算法快 3-5 倍,且通常模式串越长,算法速度越快。
1. 什么是坏字符(bad character)
首先,主串和模式串头部对齐,从尾部开始比较。上图中 S 和 E 不匹配,我们就称 S 为坏字符,即不匹配的字符。此时 S 也不包含在模式串中,因此可以直接移动到 S 的后一位。如下图所示:
依然从尾部开始比较,发现 P 与 E 不匹配,所以 P 是坏字符。但是 P 包含在模式串中,所以只能将模式串后移 2 位,两个 P 对齐。如下图所示:
由此总结出坏字符规则:后移位数 = 坏字符在模式串中的位置 - 坏字符在模式串中的上一次出现位置。如果坏字符不包含在模式串中,则上一次出现位置为 -1。
上一次出现位置:指最右出现的位置,即从模式串的当前位置开始,从右往左查找
以 P 为例,它作为坏字符,出现在模式串的第 6 位(从0开始编号),在模式串中的上一次出现位置为 4,所以后移 6 - 4 = 2 位。再以 S 为例,它出现在模式串的第 6 位,上一次出现位置是 -1(即未出现),所以后移 6 - (-1) = 7位,刚好是模式串的长度。
2. 什么是好后缀(good suffix)
依然从尾部开始比较,MPLE 与 MPLE 匹配。我们就称 MPLE、PLE、LE、E 为好后缀,即所有尾部匹配的字符串。继续比较前一位,发现 I 与 A 不匹配,所以 I 是坏字符,按照坏字符规则,应该将模式串后移 2 - (-1) = 3 位。但是,我们这里采用好后缀规则:后移位数 = 好后缀在模式串中的位置 - 好后缀在模式串中上一次出现的位置。如果好后缀在模式串中只出现一次,则上一次出现位置为 -1。
上一次出现位置:指最左出现的位置,即从模式串的头部开始,从左往右查找
BM 算法的基本思想是,每次后移这两个规则之中的较大值。更巧妙的是,这两个规则的移动位数,只与模式串有关,与主串无关。因此,可以预先计算生成《坏字符规则表》和《好后缀规则表》。使用时,只要查表比较一下就可以了。
此时,所有的好后缀之中,只有 E 还出现在 EXAMPLE 的头部,所以后移 6 - 0 = 6 (6 > 3) 位。如下图所示:
参考:BM算法思想及示例:阮一峰、BM算法实现
public class BM {
// BM算法匹配字符串,匹配成功返回P在S中的首字符下标,匹配失败返回-1
public static int indexOf(String source, String pattern) {
char[] src = source.toCharArray();
char[] ptn = pattern.toCharArray();
int sLen = src.length;
int pLen = ptn.length;
// 模式串为空字符串,返回0
if (pLen == 0) {
return 0;
}
// 主串长度小于模式串长度,返回-1
if (sLen < pLen) {
return -1;
}
int[] BC = buildBadCharacter(ptn);
int[] GS = buildGoodSuffix(ptn);
// 从尾部开始匹配,其中i指向主串,j指向模式串
for (int i = pLen - 1; i < sLen; ) {
int j = pLen - 1;
for (; src[i] == ptn[j]; i--, j--) {
if (j == 0) { // 匹配成功返回首字符下标
return i;
}
}
// 每次后移“坏字符规则”和“好后缀规则”两者的较大值
// 注意此时i(坏字符)已经向前移动,所以并非真正意义上的规则
i += Math.max(BC[src[i]], GS[pLen - 1 - j]);
}
return -1;
}
// 坏字符规则表
private static int[] buildBadCharacter(char[] pattern) {
int pLen = pattern.length;
final int CHARACTER_SIZE = 256; // 英文字符的种类,2^8
int[] BC = new int[CHARACTER_SIZE]; // 记录坏字符出现时后移位数
Arrays.fill(BC, pLen); // 默认后移整个模式串长度
for (int i = 0; i < pLen - 1; i++) {
int ascii = pattern[i]; // 当前字符对应的ASCII值
BC[ascii] = pLen - 1 - i; // 对应的后移位数,若重复则以最右边为准
}
return BC;
}
// 非真正意义上的好字符规则表,后移位数还加上了当前好后缀的最大长度
private static int[] buildGoodSuffix(char[] pattern) {
int pLen = pattern.length;
int[] GS = new int[pLen]; // 记录好后缀出现时后移位数
int lastPrefixPos = pLen; // 好后缀的首字符位置
for (int i = pLen - 1; i >= 0; i--) {
// 判断当前位置(不含)之后是否是好后缀,空字符也是好后缀
if (isPrefix(pattern, i + 1)) {
lastPrefixPos = i + 1;
}
// 如果是好后缀,则GS=pLen,否则依次为pLen+1、pLen+2、...
GS[pLen - 1 - i] = lastPrefixPos - i + pLen - 1;
}
// 上面在比较好后缀时,是从模式串的首字符开始的,但实际上好后缀可能出现在模式串中间。
// 比如模式串EXAMPXA,假设主串指针在比较P时发现是坏字符,那么XA就是好后缀,
// 虽然它的首字符X与模式串的首字符E并不相等。此时suffixLen=2表示将主串指针后移至模式串末尾,
// pLen-1-i=4表示真正的好字符规则,同样主串指针后移,使得模式串前面的XA对齐主串的XA
for (int i = 0; i < pLen - 1; i++) {
int suffixLen = suffixLength(pattern, i);
GS[suffixLen] = pLen - 1 - i + suffixLen;
}
return GS;
}
// 判断是否是好后缀,即模式串begin(含)之后的子串是否匹配模式串的前缀
private static boolean isPrefix(char[] pattern, int begin) {
for (int i = begin, j = 0; i < pattern.length; i++, j++) {
if (pattern[i] != pattern[j]) {
return false;
}
}
return true;
}
// 返回模式串中以pattern[begin](含)结尾的后缀子串的最大长度
private static int suffixLength(char[] pattern, int begin) {
int suffixLen = 0;
int i = begin;
int j = pattern.length - 1;
while (i >= 0 && pattern[i] == pattern[j]) {
suffixLen++;
i--;
j--;
}
return suffixLen;
}
}