字符串匹配算法 - BM算法
Posted huany
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了字符串匹配算法 - BM算法相关的知识,希望对你有一定的参考价值。
BM算法原理分析
BM 算法包含两部分,分别是 坏字符规则(bad character rule)和 好后缀规则(good suffix shift)
1.坏字符规则
我们从模式串的末尾往前倒着匹配,当我们发现某个字符没法匹配的时候。我们把这个没有匹配的字符叫做** 坏字符 **(主串中的字符)
当发生不匹配的时候,我们把坏字符对应的模式串中的字符下标记做si。如果坏字符在模式串中存在,我们把这个坏字符在模式串中的下标记做xi。如果不存在,我们把xi记做-1。那模式串往后移动的位数就等于si-xi。(注意,这里说的下标,都是字符在模式串的下标)
不过,单纯使用坏字符规则还是不够的。因为根据 si-xi 计算出来的移动位数,有可能是负数,比如主串是 aaaaaaaaaaaaaaaa,模式串是 baaa。不但不会向后滑动模式串,还有可能倒退。所以,BM 算法还需要用到“好后缀规则”。
“坏字符规则”本身不难理解。当遇到坏字符时,要计算往后移动的位数 si-xi,其中 xi 的计算是重点,我们如何求得 xi 呢?
如果我们拿坏字符,在模式串中顺序遍历查找,这样就会比较低效,势必影响这个算法的性能。有没有更加高效的方式呢?我们之前学的散列表,这里可以派上用场了。我们可以将模式串中的每个字符及其下标都存到散列表中。这样就可以快速找到坏字符在模式串的位置下标了。
//模式串的hashtable 记录每个字符的index
private void generateBC(char[] b, int m, int[] bc) {
for (int i = 0; i < SIZE; i++) {
bc[i] = -1;
}
for (int i = 0; i < m; i++) {
int ascii = (int) b[i];
bc[ascii] = i;
}
}
public int bm(char[] a, int n, char[] b, int m) {
int[] bc = new int[SIZE]; // 记录模式串中每个字符最后出现的位置
generateBC(b, m, bc); // 构建坏字符哈希表
int i = 0; // i 表示主串与模式串对齐的第一个字符
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; // 匹配成功,返回主串与模式串第一个匹配的字符的位置
}
// 这里等同于将模式串往后滑动 j-bc[(int)a[i+j]] 位
i = i + (j - bc[(int)a[i+j]]);
}
return -1;
}
2.好后缀规则
我们把已经匹配的 bc 叫作好后缀,记作{u}。我们拿它在模式串中查找,如果找到了另一个跟{u}相匹配的子串{u},那我们就将模式串滑动到子串{u}与主串中{u}对齐的位置。
如果在模式串中找不到另一个等于{u}的子串,我们就直接将模式串,滑动到主串中{u}的后面,因为之前的任何一次往后滑动,都没有匹配主串中{u}的情况。
不过,当模式串中不存在等于{u}的子串时,我们直接将模式串滑动到主串{u}的后面。这样做是否有点太过头呢?我们来看下面这个例子。这里面 bc 是好后缀,尽管在模式串中没有另外一个相匹配的子串{u*},但是如果我们将模式串移动到好后缀的后面,如图所示,那就会错过模式串和主串可以匹配的情况。
如果好后缀在模式串中不存在可匹配的子串,那在我们一步一步往后滑动模式串的过程中,只要主串中的{u}与模式串有重合,那肯定就无法完全匹配。但是当模式串滑动到前缀与主串中{u}的后缀有部分重合的时候,并且重合的部分相等的时候,就有可能会存在完全匹配的情况。
- 所以,针对这种情况,我们不仅要看好后缀在模式串中,是否有另一个匹配的子串,我们还要考察好后缀的后缀子串,是否存在跟模式串的前缀子串匹配的。
所谓某个字符串 s 的后缀子串,就是最后一个字符跟 s 对齐的子串,比如 abc 的后缀子串就包括 c, bc。所谓前缀子串,就是起始字符跟 s 对齐的子串,比如 abc 的前缀子串有 a,ab。我们从好后缀的后缀子串中,找一个最长的并且能跟模式串的前缀子串匹配的,假设是{v},然后将模式串滑动到如图所示的位置。
- 坏字符和好后缀的基本原理都讲完了,我现在回答一下前面那个问题。当模式串和主串中的某个字符不匹配的时候,如何选择用好后缀规则还是坏字符规则,来计算模式串往后滑动的位数?
我们可以分别计算好后缀和坏字符往后滑动的位数,然后取两个数中最大的,作为模式串往后滑动的位数。这种处理方法还可以避免我们前面提到的,根据坏字符规则,计算得到的往后滑动的位数,有可能是负数的情况。
3.好后缀代码部分
- 定义两个数组
- 现在,我们要引入最关键的变量 suffix 数组。suffix 数组的下标 k,表示后缀子串的长度,下标对应的数组值存储的是,在模式串中跟好后缀{u}相匹配的子串{u*}的起始下标值。
如果我们只记录刚刚定义的 suffix,实际上,只能处理规则的前半部分,也就是,在模式串中,查找跟好后缀匹配的另一个子串。所以,除了 suffix 数组之外,我们还需要另外一个 boolean 类型的 prefix 数组,来记录模式串的后缀子串是否能匹配模式串的前缀子串。
我们拿下标从 0 到 i 的子串(i 可以是 0 到 m-2)与整个模式串,求公共后缀子串。如果公共后缀子串的长度是 k,那我们就记录 suffix[k]=j(j 表示公共后缀子串的起始下标)。如果 j 等于 0,也就是说,公共后缀子串也是模式串的前缀子串,我们就记录 prefix[k]=true。
// b 表示模式串,m 表示长度,suffix,prefix 数组事先申请好了
private void generateGS(char[] b, int m, int[] suffix, boolean[] prefix) {
for (int i = 0; i < m; ++i) { // 初始化
suffix[i] = -1;
prefix[i] = false;
}
for (int i = 0; i < m - 1; ++i) { // b[0, i]
int j = i;
int k = 0; // 公共后缀子串长度
while (j >= 0 && b[j] == b[m-1-k]) { // 与 b[0, m-1] 求公共后缀子串
--j;
++k;
suffix[k] = j+1; //j+1 表示公共后缀子串在 b[0, i] 中的起始下标
}
i
if (j == -1) prefix[k] = true; // 如果公共后缀子串也是模式串的前缀子串
}
}
package string;
/**
* Author :梅超凡
* Date :Created in 2019/1/9 22:07
* Desc :BM算法
* 1.利用坏字符串
* 2.利用好后缀
*/
public class MatchBaseBM {
private static final int SIZE = 256; //全局变量或成员变量
public static void main(String[] args) {
String origin = "abcwehhwkqiqur";
String pattern = "qiq";
MatchBaseBM matchBaseBM = new MatchBaseBM();
int index = matchBaseBM.bm(origin.toCharArray(), origin.length(), pattern.toCharArray(), pattern.length());
System.out.println(index);
}
//模式串的hashtable 记录每个字符的index
private void generateBC(char[] b, int m, int[] bc) {
for (int i = 0; i < SIZE; i++) {
bc[i] = -1;
}
for (int i = 0; i < m; i++) {
int ascii = (int) b[i];
bc[ascii] = i;
}
}
private void generateGS(char[] b, int m, int[] suffix, boolean[] prefix) {
for (int i = 0; i < m; ++i) {
suffix[i] = -1;
prefix[i] = false;
}
for (int i = 0; i < m - 1; i++) //b[0,i]
{
int j = i;
int k = 0; //公共后缀子串长度
while (j >= 0 && b[j] == b[m - 1 - k]) { //与b[0,m-1求公共后缀子串]
--j;
++k;
suffix[k] = j + 1; //j+1表示公共后缀子串在b[0,i]的起始位置
}
if (j == -1) prefix[k] = true;
}
}
//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;
}
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);
//i表示主串与模式串对齐的第一个字符
int i = 0;
while (i <= n - m) {
int j;
//模式串从后往前匹配
for (j = m - 1; j >= 0; --j) {
if (a[i + j] != b[j]) break;
}
if (j < 0) {
// 匹配成功,返回主串与模式串第一个匹配的字符的位置
return i;
}
//这里等同于将模式串往后移动 j-bc[(int)a[i+j]]位
int x = i + (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;
}
}
以上是关于字符串匹配算法 - BM算法的主要内容,如果未能解决你的问题,请参考以下文章