2. BM(Boyer-Moore)算法

 对于工业级的软件开发来说,我们希望算法尽可能的高效,并且在极端情况下,性能也不要退化 的太严重。那么,对于查找功能是重要功能的软件来说,比如一些文本编辑器,它们的查找功能 都是用哪种算法

来实现的呢?  有没有比 BF 算法和 RK 算法更加高效的字符串匹配算法呢?  即 BM(Boyer-Moore)算法。它是一种非常高效的字符串匹配算法,有实验统计,它的性能是著名的KMP 算法的 3 到 4 倍。

BM 算法的核心思想

模式串和主串的匹配过程,看作模式串在主串中不停地往后滑动。当遇到不匹配的字符时,BF 算法和 RK 算法的做法是,模式串往后滑动一位,然后从模式串的第一个字符开始重新匹配。

     算法 08| 字符串算法| BM| KMP 

主串中的 c,在模式串中是不存在的,所以,模式串向后滑动的时候,只要 c 与模式串有重合,肯定无法匹配。所以,可以一次性把模式串往后多滑动几位,把模式串移动 到 c 的后面。

      算法 08| 字符串算法| BM| KMP    

由现象找规律,当遇到不匹配的字符时,有什么固定的规律,可以将模式串往后多滑动几位呢?这样一次性往后滑动好几位,那匹配的效率岂不是就提高了?

BM 算法,本质上其实就是在寻找这种规律。借助这种规律,在模式串与主串匹配的过程中,当模式串和主串某个字符不匹配的时候,能够跳过一些肯定不会匹配的情况,将模式串往后多滑动几位。

BM 算法原理分析

BM 算法包含两部分,分别是坏字符规则(bad character rule)和好后缀规则(good suffix shift)

1. 坏字符规则

之前在匹配的过程中,都是按模式串的下标从小到大的顺序,依次与主串中 的字符进行匹配的。这种匹配顺序比较符合我们的思维习惯,而 BM 算法的匹配顺序比较特 别,它是按照模式串下标从大到小的顺序,倒着匹配的。

   算法 08| 字符串算法| BM| KMP

 从模式串的末尾往前倒着匹配,当发现某个字符没法匹配的时候。把这个没有匹配的字符叫作坏字符(主串中的字符)。  

拿坏字符 c 在模式串中查找,发现模式串中并不存在这个字符,也就是说,字符c 与模式串中的任何字符都不可能匹配。这个时候,我们可以将模式串直接往后滑动三位,将模式串滑动到c 后面的位置,再从模式串的末尾字符开始比较。

 算法 08| 字符串算法| BM| KMP

 发现,模式串中后一个字符 d,还是无法跟主串中的 a 匹配,这个时候,还能将模式串往后滑动三位吗?答案是不行的。因为这个时候,坏字符 a 在模式串中是存在的, 模式串中下标是 0 的位置也是字符 a。这种情况下,我们可以将模式串往后滑动两位,让两个 a 上下对齐,然后再从模式串的末尾字符开始,重新匹配。

算法 08| 字符串算法| BM| KMP

第一次不匹配的时候,滑动了三位,第二次不匹配的时候,将模式串后移两位,那具体滑动多少位,到底有没有规律呢?

当发生不匹配的时候,把坏字符对应的模式串中的字符下标记作 si如果坏字符在模式串中存在,我们把这个坏字符在模式串中的下标记作 xi。

              如果不存在,我们把 xi 记作 -1。那模式串 往后移动的位数就等于 si - xi。(注意,这里说的下标,都是字符在模式串的下标)。

算法 08| 字符串算法| BM| KMP

 如果坏字符在模式串里多处出现,那我们在计算 xi 的时候,选择靠后的那个,因为这样不会让模式串滑动过多,导致本来可能匹配的情况被滑动略过。

利用坏字符规则,BM 算法在好情况下的时间复杂度非常低,是 O(n/m)。比如,主串是 aaabaaabaaabaaab,模式串是 aaaa。每次比对,模式串都可以直接后移四位,所以,匹配具 有类似特点的模式串和主串的时候,BM 算法非常高效。

不过,单纯使用坏字符规则还是不够的。因为根据 si-xi 计算出来的移动位数,有可能是负数, 比如主串是 aaaaaaaaaaaaaaaa,模式串是 baaa。不但不会向后滑动模式串,还有可能倒退。 所以,BM 算法还需要用到“好后缀规则”

2. 好后缀规则

好后缀规则实际上跟坏字符规则的思路很类似。当模式串滑动到图中位置的时候,模式串和主串有 2 个字符是匹配的,倒数第 3 个字符发生了不匹配的情况。

 算法 08| 字符串算法| BM| KMP

  把已经匹配的 bc 叫作好后缀,记作{u}。我们拿它在模式串中查找,如果找到了另一个跟{u}相匹配的子串{u*},就将模式串滑动到子串{u*}与主串中{u}对齐的位置。
 算法 08| 字符串算法| BM| KMP

如果在模式串中找不到另一个等于{u}的子串,就直接将模式串,滑动到主串中{u}的后面,因为之前的任何一次往后滑动,都没有匹配主串中{u}的情况。
 算法 08| 字符串算法| BM| KMP

当模式串中不存在等于{u}的子串时,直接将模式串滑动到主串{u}的后面。这样做是否有点太过头呢?看下面这个例子。这里面 bc 是好后缀,尽管在模式串中没有另外一个
相匹配的子串{u*},但是如果我们将模式串移动到好后缀的后面,如图所示,那就会错过模式串和主串可以匹配的情况。
    算法 08| 字符串算法| BM| KMP

如果好后缀在模式串中不存在可匹配的子串,那在我们一步一步往后滑动模式串的过程中,只要主串中的{u}与模式串有重合,那肯定就无法完全匹配。但是当模式串滑动到前缀与主串中{u}的后缀有部分重合的时候,并且重合的部分相等的时候,就有可能会存在完全匹配的情况。
  算法 08| 字符串算法| BM| KMP

针对这种情况,我们不仅要看好后缀在模式串中,是否有另一个匹配的子串,我们还要考察好后缀的后缀子串,是否存在跟模式串的前缀子串匹配的。

所谓某个字符串 s 的后缀子串,就是最后一个字符跟 s 对齐的子串,比如 abc 的后缀子串就包括 c, bc。所谓前缀子串,就是起始字符跟 s 对齐的子串,比如 abc 的前缀子串有 a,ab。我们从好后缀的后缀子串中,找一个最长的并且能跟模式串的前缀子串匹配的,假设是{v},然后将模式串滑动到如图所示的位置。
 算法 08| 字符串算法| BM| KMP

当模式串和主串中的某个字符不匹配的时候,如何选择用好后缀规则还是坏字符规则,来计算模式串往后滑动的位数?
我们可以分别计算好后缀和坏字符往后滑动的位数,然后取两个数中最大的,作为模式串往后滑动的位数。这种处理方法还可以避免我们前面提到的,根据坏字符规则,计算得到的往后滑动的位数,有可能是负数的情况。

BM 算法代码实现

/**
 * BM 算法
 */
public class BM {
    private static final int SIZE = 256;  // 全局变量或成员变量

    /**
     * 将模式串中的每个字符及其下标都存到散列表中。这样就可以快速找到坏字符在模式串的位置下标了
     * @param b 模式串
     * @param m 模式串的长度
     * @param bc 散列表, 只实现一种简单的情况, 假设字符串的字符集不是很大, 每个字符长度是1字节,用大小为256的数组来记录每个字符在模式串中出现的位置。
     *           数组的下标对应字符的 ASCII 码值, 数组中存储这个字符在模式串中出现的位置.
     *        散列表下标为 模式串的字符所对应的ASCII数值,
     *        散列表的value值为 模式串的下标;
     */
    private void generateBC(char[] b, int m, int[] bc) {
        for (int i = 0; i < SIZE; i++) {
            bc[i] = -1;  // 初始化 bc即散列表
        }
        for (int i = 0; i < m; i++) {
            int ascii = (int) b[i]; // 计算 b[i] 的 ASCII 值
            bc[ascii] = i;
        }
    }

    /**
     * 暴力解法, 框架的搭建
     * @param a 主串
     * @param n 主串的长度
     * @param b 模式串
     * @param m 模式串的长度
     * @return
     */
    public int bm(char[] a, int n, char[] b, int m) {
        int[] bc = new int[SIZE];
        generateBC(b, m, bc);
        //i, j,双指针(头指针和 尾指针)
        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;
            }
            i = i + (j - bc[(int) a[i+j]]); //往后移动i 位
        }
        return -1;
    }

    /**
     *  suffix 数组和 prefix 数组的计算过程
     * @param b 模式串
     * @param m 模式串的长度
     * @param suffix suffix 数组的下标k 表示后缀子串的长度, 下标对应的数组值存储的是在模式串中跟好后缀{u}相匹配的子串{u*}的起始下标值
     * @param prefix  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++) {
            int j = i;
            int k = 0;
            while (j >= 0 && b[j] == b[m-1-k]) {
                j--;
                k++;
                suffix[k] = j+1;
            }
            if (j == -1)
                prefix[k] = true;
        }
    }

    // a,b 表示主串和模式串;n,m 表示主串和模式串的长度。
    public int bm2(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;
    }
    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;
    }
}
View Code

相关文章:

  • 2022-12-23
  • 2022-12-23
  • 2021-10-11
  • 2022-01-22
  • 2021-11-24
  • 2021-10-14
  • 2022-12-23
猜你喜欢
  • 2021-12-07
  • 2021-06-16
  • 2021-11-20
  • 2021-09-04
  • 2021-06-27
  • 2021-11-13
  • 2021-10-27
相关资源
相似解决方案