【算法】Boyer-Moore 算法

目录

  • 1.概述
    • 1.1.Boyer-Moore 算法介绍
    • 1.2.坏字符规则表
    • 1.3.好后缀规则表
    • 1.4.总结
  • 2.代码实现
  • 3.应用

更多数据结构与算法的相关知识可以查看数据结构与算法这一专栏。

有关字符串模式匹配的其它算法:
【算法】Brute-Force 算法
【算法】KMP 算法
【算法】Rabin-Karp 算法

1.概述

1.1.Boyer-Moore 算法介绍

(1)Boyer-Moore 算法又称为 Boyer-Moore 字符串搜索算法(下文简称为 BM 算法),由 Robert S. Boyer 和 J Strother Moore 于1977年提出,它是一种高效的字符串匹配算法,用于在一个目标串中查找一个模式串的出现位置。它的核心思想是通过预处理模式串,利用字符比较的不匹配信息来跳过尽可能多的目标字符,从而快速定位可能的匹配位置,以减少比较次数

(2)BM 算法的主要思想是从模式串(要搜索的字符串)的末尾开始匹配,然后根据不匹配字符在模式串中的位置,跳过一些不必要的比较。具体来说,BM 算法通过预处理模式串,构建两个表:

  • 坏字符规则表:用于记录模式串中每个字符的最后出现位置。在匹配过程中,当发生不匹配时,根据坏字符规则表决定将模式串向右滑动 badShift 位。
  • 好后缀规则表:记录模式串中每个后缀子串的匹配长度。当模式串的后缀子串与主串的某个子串匹配时,可以使用好后缀规则表决定将模式串向右滑动 goodShift 位。

当发生不匹配时,模式串向右滑动的位数为 max(badShift, goodShiftf)。下面将具体介绍这两种规则表。

1.2.坏字符规则表

(1)当目标串中的某个字符跟模式串的某个字符不匹配时,我们称目标串中的这个不匹配字符为坏字符,此时模式串需要向右移动 badShift 位,并且 badShift = 当前对比的字符在模式串中的位置 - 坏字符在模式串中最右出现的位置,如果该模式串中不存在该坏字符,那么最右出现的位置设置为 -1。例如,在下图中,从模式串的末尾开始匹配,显然字符 F 与 字符 B 不匹配,因此字符 F 便为坏字符。
在这里插入图片描述

(2)而上面模式串中并没有字符 F,因此由上面的公式可得 badShift = 5 - (-1) = 6,如下图所示:

在这里插入图片描述

(3)之所以在遇到坏字符时模式串需要向后移动 badShift 位,其原因在于对于当前坏字符来说:

  • 如果模式串中没有该坏字符,那么说明模式串中的任何一个字符都不可能与其匹配成功,所以此时模式串的第一个字符应该直接目标串中该坏字符的后一个字符对齐,即上图中模式串中的字符 A 与目标串中的字符 C 对齐,然后再进行匹配;
  • 如果模式串中有该坏字符,我们需要选取模式串中最右出现的该坏字符,并且将它们对齐。之所以要选取最右字符,其原因在于防止漏掉正确答案,具体例子如下:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

(4)在代码实现中,我们可以用数组来存储模式串中每个字符最右出现的下标,并且模式串中不存在的字符的下标默认为 -1。而坏字符规则表的大小一般为 256,其原因在于在 ASCII 编码中,总共有 256 个字符,每个字符的取值范围是 0 - 255。通过使用 256 大小的坏字符规则表,可以覆盖所有可能的字符。

1.3.好后缀规则表

(1)在 BM 算法中,好后缀是指在模式串中与主串匹配的后缀子串,例如下图中的 “ABC” 即为好后缀。

在这里插入图片描述

(2)而好后缀规则表 goodTable 一般用数组表示,其长度等于模式串的长度(记为 m),并且 goodTable[i] 表示好后缀 t[i + 1…m - 1] 的后缀子串能够匹配模式串中的其它最长子串 substring 时,模式串需要移动的位数,我们需要注意:

  • 如果是好后缀本身与 substring 匹配,那么对 substring 没有什么限制(当然不能是好后缀本身),但是当存在多个匹配的 substring 时,我们应该选择最右出现的 substring,其理由与上面类似,主要是为了防止漏掉正确答案。
  • 如果是好后缀的后缀子串且不是好后缀本身与 substring 匹配,那么 substring 必须是模式串的前缀子串,其原因在于如果 substring 不是前缀子串(它一定是最长的),那么它的前一个字符必然和与其匹配的后缀子串的前一个字符不相等,这样一来滑动到当前位置的模式串就没有匹配的必要了。

例如,上图中好后缀 “ABC” 以及其后缀子串指 “ABC”、“C”、“BC” 这三个字符串,那么显然它们能够匹配模式串的其它最长子串为 “BC”(由于是后缀子串 “BC” 进行匹配的,因此 “BC” 必须是模式串的前缀子串),即 goodTable[3] = 5(此处的下标 3 对应模式串中的字符 D 的下标),如下图所示:
在这里插入图片描述

再例如,下图中好后缀 “ABC” 显然可以与模式串中的最长子串 “ABC”(绿色填充部分)匹配,即 goodTable[3] = 3(此处的下标 3 对应模式串中的字符 C 的下标)。

在这里插入图片描述

在这里插入图片描述

(3)虽然可以直接使用 goodTable 记录遇到好后缀时模式串需要移动的位数(其代码实现如下),但是求解 goodTable 的时间复杂度却达到了 O(n3)(两层 for 循环加上 equals 方法),这样一来效率较低。

public int[] getGoodTable(String t) {
    int m = t.length();
    int[] goodTable = new int[m];
    Arrays.fill(goodTable, m);
    for (int i = 1; i < m; i++) {
        // suffix 为好后缀 t[i...m - 1]
        String suffix = t.substring(i);
        // suffix 长度
        int suffixLen = m - i;
        for (int j = m - 1; j >= m - suffixLen; j--) {
            if (j >= suffixLen) {
                //判断好后缀本身是否与其它子串匹配
                if (t.substring(j - suffixLen, j).equals(suffix)) {
                    goodTable[m - i - 1] = m - j;
                    break;
                }
            } else {
                //判断好后缀的后缀子串是否与前缀子串匹配
                if (t.substring(0, j).equals(suffix.substring(suffixLen - j))) {
                    goodTable[m - i - 1] = m - j;
                    break;
                }
            }
        }
    }
    return goodTable;
}

(4)实际上,我们可以使用另一种方法将时间复杂度降低到 O(n2),我们可以使用长度为 m 的数组 suffix 来保存每个不同长度的好后缀匹配的其它最长靠右子串的起始位置,以模式串 t = “ABCDABC” 为例:

好后缀长度suffix解释
C1suffix[1] = 2长度为 1 的好后缀 C 与起始下标为 2 的子串 C 匹配,并且满足最长和靠右这两个要求
BC2suffix[2] = 1长度为 2 的好后缀 BC 与起始下标为 1 的子串 BC 匹配,并且满足最长和靠右这两个要求
ABC3suffix[3] = 0长度为 3 的好后缀 ABC 与起始下标为 0 的子串 BC 匹配,并且满足最长和靠右这两个要求
DABC4suffix[4] = -1好后缀 DABC 不与任何其它子串匹配,标记为 -1
CDABC5suffix[5] = -1好后缀 CDABC 不与任何其它子串匹配,标记为 -1
BCDABC6suffix[6] = -1好后缀 BCDABC 不与任何其它子串匹配,标记为 -1

求解数组 suffix 的代码如下,其时间复杂度为 O(n2)。

int m = t.length();
int[] suffix = new int[m];
Arrays.fill(suffix, -1);
for (int i = 0; i < m - 1; i++) {
    int j = i;
    int k = 0;
    while (j >= 0 && t.charAt(j) == t.charAt(m - 1 - k)) {
        j--;
        k++;
        suffix[k] = j + 1;
    }
}

(5)如果我们通过数组 suffix 知道了当前好后缀并不能匹配其它子串时,那么如何判断它的后缀子串是否可以与模式串的前缀子串进行匹配呢?其实,解决办法也比较简单:

  • 如果 suffix[k] != -1,那么说明当前好后缀 t[j + 1...m - 1] 可以匹配其它最长靠右子串,并且该子串的起始位置为 suffix[k],此时模式串需要向右移动 goodShift = j - suffix[k] + 1 位,其中 j 与好后缀长度 k 的关系为 k = m - 1 - j
  • 如果 suffix[k] == -1,那么说明当前好后缀 t[j + 1...m - 1] 没有可以匹配的其它最长靠右子串,此时,我们需要从该好后缀的最长后缀子串 t[r...m - 1] 开始遍历(其中 r = j + 2),寻找与之匹配的最长前缀。判断方法也比较简单,即判断 suffix[m - r] 是否为 0 即可:
    • 如果 suffix[m - r] == 0,则说明该后缀子串与长度为 m - r 前缀的匹配,此时模式串需要向右移动 goodShift = r 位;
    • 如果 suffix[m - r] != 0,则说明该后缀子串与长度为 m - r 前缀的不匹配,判断下一个后缀子串即可;
  • 如果上面两种情况都没有匹配成功,则直接返回模式串的长度,具体代码如下所示:
private static int moveByGs(int j, int m, int[] suffix) {
    // k 为当前好后缀 t[j + 1...m - 1] 的长度
    int k = m - 1 - j;
    if (suffix[k] != -1) {
        //好后缀可以直接匹配
        return j - suffix[k] + 1;
    }
    //好后缀不能直接匹配,那么寻找与好后缀的后缀子串匹配的模式串的最长前缀
    for (int r = j + 2; r <= m - 1; r++) {
        //判断后缀子串 t[r...m - 1] 匹配的最长靠右子串的起始位置是否为 0
        if (suffix[m - r] == 0) {
            return r;
        }
    }
    //上面两种情况都没有匹配成功,则直接返回模式串的长度
    return m;
}

1.4.总结

在匹配过程中发生不匹配时,我们可以根据上面计算得到的坏字符规则表和好后缀规则表,来分别计算模式串应该向右滑动的最大位数 badShiftgoodShift,并且都能保证不会漏掉正确答案。所以在 BM 算法中,每次发生不匹配时,模式串最终向右滑动的位数为 max(badShift, goodShift),即两者之间的最大值。

2.代码实现

(1)BM 算法的代码实现如下:

class BmAlgorithm {
    private final static int ASCII_SIZE = 256;

    public static int bmSearch(String s, String t) {
        int n = s.length();
        int m = t.length();
        if (m == 0) {
            return 0;
        }

        //坏字符规则表 badTable: 记录模式串中每个字符最后出现的位置
        int[] badTable = new int[ASCII_SIZE];
        Arrays.fill(badTable, -1);
        for (int i = 0; i < t.length(); i++) {
            badTable[t.charAt(i)] = i;
        }

        /*
        * 好后缀规则表 goodTable: 用数组 suffix 表示
        * suffix[i] 表示长度为 i 的好后缀匹配的最长靠右子串的起始位置
        * */
        int[] suffix = new int[m];
        Arrays.fill(suffix, -1);
        for (int i = 0; i < m - 1; i++) {
            int j = i;
            int k = 0;
            while (j >= 0 && t.charAt(j) == t.charAt(m - 1 - k)) {
                j--;
                k++;
                suffix[k] = j + 1;
            }
        }

        int i = 0;
        while (i <= n - m) {
            //从右往左开始匹配
            int j = m - 1;
            while (j >= 0 && t.charAt(j) == s.charAt(i + j)) {
                j--;
            }
            if (j < 0) {
                //匹配成功
                return i;
            } else {
                //根据坏字符规则计算要移动的位数
                int badShift = j - badTable[s.charAt(i + j)];
                //根据好字符规则计算要移动的位数
                int goodShift = 0;
                //如果 j == m - 1,即好后缀为 "",此时需要遵循坏字符规则
                if (j < m - 1) {
                    goodShift = moveByGs(j, m, suffix);
                }
                i += Math.max(badShift, goodShift);
            }
        }
        //无法匹配
        return -1;
    }

    private static int moveByGs(int j, int m, int[] suffix) {
        // k 为当前好后缀 pattern[j + 1...m - 1] 的长度
        int k = m - 1 - j;
        if (suffix[k] != -1) {
            //好后缀可以直接匹配
            return j - suffix[k] + 1;
        }
        //好后缀不能直接匹配,那么寻找与好后缀的后缀子串匹配的模式串的最长前缀
        for (int r = j + 2; r <= m - 1; r++) {
            //判断后缀子串 pattern[r...m - 1] 匹配的最长靠右子串的起始位置是否为 0
            if (suffix[m - r] == 0) {
                return r;
            }
        }
        //上面两种情况都没有匹配成功,则直接返回模式串的长度
        return m;
    }
}

(2)测试代码如下:

class BmAlgorithmTest {
	public static void main(String[] args) {
        String text = "hello world AABCABC";
        String pattern = "ABCDABC";
        int index = bmSearch(text, pattern);
        if (index != -1) {
            System.out.println("Pattern found at index: " + index);
        } else {
            System.out.println("Pattern not found.");
        }
    }
}

输出结果为:

Pattern found at index: 16

注意:如果想找出所有匹配成功的位置,只需要简单改一下上面的代码即可,当匹配成功时,我们可以使用 list 来存储当前匹配成功的位置,当匹配结束后,返回 list 即可。

3.应用

BM 算法适用于各种类型的字符串匹配问题,具体来说,BM算法在以下情况下表现优秀:

  • 目标串较长:BM 算法适用于处理大规模文本串的情况,因为它能够利用坏字符规则和好后缀规则在较少的比较操作中快速跳过大量的字符。
  • 模式串较短:相对于文本串,模式串较短的情况下,BM 算法的性能更为突出,因为它能够利用坏字符规则和好后缀规则快速跳过无需比较的部分。
  • 重复字符较少:如果模式串中包含大量重复字符,BM 算法的好后缀规则会出现退化的情况,导致性能下降。因此,当模式串中的重复字符较少时,BM算法能够发挥最佳性能。

总之,BM算法适用于大规模文本串和较短、重复字符较少的模式串的字符串匹配问题。它在实际中被广泛应用于文本编辑器、搜索引擎、数据处理等领域。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/219146.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Mac电脑vm虚拟机 VMware Fusion Pro中文 for mac

VMware Fusion Pro是一款功能强大的虚拟机软件&#xff0c;适用于需要在Mac电脑上运行其他操作系统的用户。它具有广泛的支持、快速稳定的特点以及多种高级功能&#xff0c;可以满足用户的各种需求和场景。 多操作系统支持&#xff1a;VMware Fusion Pro允许在Mac电脑上运行多…

合理的从度设置参数

环境&#xff1a;主库双1模式 一。单SQL线程 1.pos模式 1.1 position mode 模式&#xff08;最安全&#xff09; master_info_repository table relay_log_info_repository table recovery_relay_log off sync_master_info 1 sync_relay_log 1 sync_relay_log_in…

完善根文件系统

一. 简介 本文完善之前创建的根文件系统。 上一篇文章通过 设置 bootargs参数&#xff0c;使开发板通过 nfs服务从 ubuntu系统加载根文件系统。文章地址如下&#xff1a; 根文件系统初步测试-CSDN博客 二. 完善根文件系统 上一篇文章通过 设置 bootargs参数&#xff0c;使…

C++笔试题之回文数的判断

“回文”是指正读反读都能读通的句子&#xff0c;它是古今中外都有的一种修辞方式和文字游戏&#xff0c;如“我为人人&#xff0c;人人为我”等。在数学中也有这样一类数字有这样的特征&#xff0c;成为回文数&#xff08;palindrome number&#xff09;。 设n是一任意自然数…

创意设计利器:分享六款最佳的平面设计软件

1. 即时设计 即时设计是一种基于云的在线协作设计工具&#xff0c;具有原型、设计、交付、管理等全栈能力&#xff0c;对于个人及中小团队永久免费&#xff0c;至今已成为越来越多设计师的首选。 矢量编辑&#xff1a;即时设计允许用户创建、编辑和操纵矢量图形。它提供了各种…

外包干了4年,技术退步明显...

先说情况&#xff0c;大专毕业&#xff0c;18年通过校招进入湖南某软件公司&#xff0c;干了接近4年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能测试&#xf…

基因组变异注释 — ANNOVAR(一)

基因组变异注释 — ANNOVAR&#xff08;一&#xff09; 1 简介 ANNOVAR 是一款高效的基因组注释工具&#xff0c;专门用于分析和注释来自多种生物基因组&#xff08;包括人类的 hg18、hg19、hg38&#xff0c;以及小鼠、蠕虫、果蝇、酵母等&#xff09;的遗传变异。这个工具实际…

【工作生活】汽车电子嵌入式开发简介

目录 1. 目标 2. 要分享什么 3.1 行业知识 3.1.1车载行业知识&#xff1a; 3.1.2项目&#xff1a; 3.1.3开发测试工具&#xff1a; 3.2 硬件平台 3.3 基础知识 3.4 工作生活 3. 我们是谁 1. 目标 随着新能源汽车的快速崛起&#xff0c;汽车电子行业开始快速发展&…

iEnglish:家长陪伴助力养成阅读兴趣与坚持习惯

日前,一则10岁男孩子铮连续进行英语原版阅读超过940天的打卡视频在短视频平台刷屏,评论区成为家长热议互动的平台,高赞评论包括“孩子是如何做到愿意阅读的?”“口语太棒了!”“别人家的孩子就是优秀!”“求子铮妈妈分享经验”等等。 子铮妈妈对于自己孩子的优秀也是十分开心…

通达信指标公式19:龙虎榜股票池——主力控盘度的计算方法

0.小红牛本指标&#xff0c;选股的思路说明&#xff1a;控盘度&#xff0c;又称主力控盘&#xff0c;是指主力控制了某只股票的大部分流通股&#xff0c;从而控制了股票的价格。主力控盘的目的通常是为了获取更多的收益&#xff0c;通过控制股票价格来实现其策略。所以首要分析…

数据结构——链表题目

文章目录 JZ25 合并两个排序的链表&#xff08;简单&#xff09;NC22 合并两个有序的数组&#xff08;简单&#xff09;NC3 链表中环的入口节点&#xff08;中等&#xff09;NC50 链表中的节点每k个一组翻转&#xff08;中等&#xff09;NC53 删除链表的倒数第n个节点(中等) JZ…

软件崩溃时VS中看不到有效的调用堆栈,使用Windbg动态调试去分析定位

目录 1、问题说明 2、使用Windbg查看崩溃时详细的函数调用堆栈 3、将Windbg中显示的函数调用堆栈对照着C源码进一步分析 4、最后 VC常用功能开发汇总&#xff08;专栏文章列表&#xff0c;欢迎订阅&#xff0c;持续更新...&#xff09;https://blog.csdn.net/chenlycly/art…

[Java学习日记]多线程练习、线程池

目录 一.案例&#xff1a;五个人抢红包 二.案例&#xff1a;两个抽奖池抽奖 三.案例&#xff1a;两个抽奖池抽奖&#xff1a;获取线程运行的结果 四.线程池&#xff1a;用来存放线程&#xff0c;避免多次重复创建线程 五.自定义线程池 六.最大并行数与线程池大小 一.案例&…

【Android】Glide的简单使用(上)

文章目录 引入Glide的优点:缺点: 使用常用方法:从网络加载图片从文件加载图片加载resource资源加载URI地址设置占位图出错时的图片占位图图片过渡的Transitions自定义过渡动画图片大小调整缩放图片播放gifasGif()把Gif当作Bitmap播放显示本地视频缩略图 引入 Glide是Google员工…

IntelliJ IDEA 2023.2新特性详解第三弹!Docker、Kubernetes等支持!

9 Docker 在 Docker 镜像层内预览文件 现在可以在 Services&#xff08;服务&#xff09;工具窗口中轻松访问和预览 Docker 镜像层的内容。 从列表选择镜像&#xff0c;选择 Show layers&#xff08;显示层&#xff09;&#xff0c;然后点击 Analyze image for more informati…

平价开放式耳机怎么选?盘点几款好用的平价开放式耳机!

在这个充满音频奇迹的年代&#xff0c;选一副好耳机就像是挑选人生伴侣一样重要&#xff0c;而开放式耳机&#xff0c;由于佩戴无需入耳带来了不错的舒适度&#xff0c;就此受到了许多人的喜爱。 可问题是&#xff0c;市面上平价开放式耳机太多&#xff0c;让人眼花缭乱&#…

医院运维 告警闪现后的故障排查

长期以来&#xff0c;医院信息化运维中存在着科室复杂、应用场景多、终端运维工作量大、软件系统兼容需求强等诸多痛点&#xff0c;且对技术设备的稳定性、连续性要求极高&#xff0c;在日常运维中&#xff0c;需要应对和解决这些问题来保障业务稳定、健康运行。 1、数据孤岛 …

【离散数学】——期末刷题题库(二元关系作业一(运算性质闭包))

&#x1f383;个人专栏&#xff1a; &#x1f42c; 算法设计与分析&#xff1a;算法设计与分析_IT闫的博客-CSDN博客 &#x1f433;Java基础&#xff1a;Java基础_IT闫的博客-CSDN博客 &#x1f40b;c语言&#xff1a;c语言_IT闫的博客-CSDN博客 &#x1f41f;MySQL&#xff1a…

揭秘MQTT:为何它是物联网的首选协议?

文章目录 MQTT 协议简介概览MQTT 与其他协议对比MQTT vs HTTPMQTT vs XMPP 为什么 MQTT 是适用于物联网的最佳协议&#xff1f;轻量高效&#xff0c;节省带宽可靠的消息传递海量连接支持安全的双向通信在线状态感知 MQTT 5.0 与 3.1.1MQTT 服务器MQTT 客户端 MQTT 协议简介 概…

acwing1209.带分数暴力与优化(java版)

//n a b / c n是确定的,只需找到其中两个。判断剩下一个数是否满足条件即可 //由题目条件可知,每个数不能重复使用,需要一个st全局数组判断每个数是否使用过 //递归实现排列型枚举,cn ac b //对于枚举出来的每一个a,再去枚举每一个c,再在c的枚举里判断b是否满足条件 //…