转自:Boyer-Moore算法

一.简述

在当前用于查找子字符串的算法中,BM(Boyer-Moore)算法是当前有效且应用比较广的一中算法,各种文本编辑器的“查找”功能(Ctrl+F),大多采用Boyer-Moore算法。比我们在学习的KMP算法快3~5倍。

Boyer-Moore算法不仅效率高,而且构思巧妙,容易理解。1977年,德克萨斯大学的Robert S. Boyer教授和J Strother Moore教授发明了这种算法。

二.算法思想

我们知道常规的字符串匹配算法是从左往右的,这也比较符合我们一贯的思维,但是BM算法是从右往左的。一般匹配我们用的是蛮力匹配,而经典的BM算法其实是对后缀蛮力匹配算法的改进,下面我们给出蛮力后缀匹配的伪代码。

  1. j = 0;
  2. while (j <= strlen(T) - strlen(P)) {
  3. for (i = strlen(P) - 1; i >= 0 && P[i] ==T[i + j]; --i)
  4. if (i < =0)
  5. match;
  6. else
  7. ++j;
  8. }

从上面的伪代码中我们可以看出每当失匹的时候,就会往后移一位,也就是上面++j这一行代码;而BM算法所做的就是改进这一行代码,即模式串不在每次只移动一步,而是根据已经匹配的后缀信息,来判断移动的距离,通常80%左右能够移动模式串的长度,从而可以跳过大量不必须比较的字符,大大提高了查找效率。

为了实现更快的移动模式串,BM定义了两个规则,坏后缀规则和好后缀规则。这两个规则分别计算我们能够向后移动模式串长度,然后选取这两个规则中移动大的,作为我们真正移动的距离。也就是上述伪代码中j不在每次加一,而是加上上面两个规则中移动长度大的。

假定字符串为”HERE IS A SIMPLE EXAMPLE”,模式串为”EXAMPLE”。下面我将阐述几个概念,坏字符和好后缀。

上图中我们看到,”S”与”E”不匹配。这时,“S”就被称为”坏字符”(bad character),即不匹配的字符。

上图中 ”MPLE”与”MPLE”匹配。我们把这种情况称为”好后缀”(good suffix),即所有尾部匹配的字符串。注意,”MPLE”、”PLE”、”LE”、”E”都是好后缀,这点后面我们会用到。

坏字符算法

当出现一个坏字符时, BM算法向右移动模式串, 让模式串中最靠右的对应字符与坏字符相对,然后继续匹配。坏字符算法有两种情况。

  • 模式串中有对应的坏字符时,y为字符串,x为模式串,见下图

坏字符出现在模式串中,这时可以把模式串第一个出现的坏字符和母串的坏字符对齐,也就是上面所说的最靠右。当然,这样可能造成模式串倒退移动,因为坏字符可能出现在与模式串失匹位置的右面,不过由于我们移动不光看坏后缀还看好后缀,所以不会后退。

  • 模式串中不存在坏字符,这时可以把模式串移动到坏字符的下一个字符,继续比较。如下图所示

好后缀算法

好后缀算法分为三种情况

  • 模式串中有子串匹配上好后缀,此时移动模式串,让该子串和好后缀对齐即可,如果超过一个子串匹配上好后缀,则选择最靠右边的子串对齐,防止有漏匹配的。

  • 模式串中没有子串匹配上后后缀,此时需要寻找模式串的一个最长前缀,并让该前缀等于好后缀的后缀,寻找到该前缀后,让该前缀和好后缀对齐即可。

  • 模式串中没有子串匹配上后后缀,并且在模式串中找不到最长前缀,让该前缀等于好后缀的后缀。此时,直接移动模式到好后缀的下一个字符。

BM算法的大体思想到这里我们基本介绍结束了,下面我们通过一个例子来具体感受一下

三.例子

下面,我根据Moore教授自己的例子来解释这种算法。希望通过例子大家能有一个感性的认识。

1.

假定字符串为”HERE IS ASIMPLE EXAMPLE”,搜索词为”EXAMPLE”。搜索词我们下面都称为模式串。

2.

首先,”字符串”与”模式串”头部对齐,从尾部开始比较。

我们看到,”S”与”E”不匹配。这时,“S”就被称为”坏字符”(badcharacter),即不匹配的字符。我们还发现,”S”不包含在模式串”EXAMPLE”之中,这意味着可以把模式串直接移到”S”的后一位。这里适用坏规则。

3.

依然从尾部开始比较,发现”P”与”E”不匹配,所以”P”是”坏字符”。但是,”P”包含在模式串”EXAMPLE”之中。所以,根据坏规则将模式串中的最右的“P”与字符串中的”P”对齐,模式串后移两位。

4.

我们由此总结出“坏字符规则”:

  后移位数 = 坏字符的位置 – 搜索词中的上一次出现位置

如果”坏字符”不包含在搜索词之中,则上一次出现位置为 -1。

以”P”为例,它作为”坏字符”,出现在搜索词的第6位(从0开始编号),在搜索词中的上一次出现位置为4,所以后移 6 – 4 = 2位。再以前面第二步的”S”为例,它出现在第6位,上一次出现位置是 -1(即未出现),则整个搜索词后移 6 – (-1) = 7位。

5.

E和E匹配,继续匹配

比较前一位,LE和LE匹配

比较前一位,PLE与PLE匹配

比较前面一位,”MPLE”与”MPLE”匹配。我们把这种情况称为”好后缀”(good suffix),即所有尾部匹配的字符串。注意,”MPLE”、”PLE”、”LE”、”E”都是好后缀。

6.

比较前一位,发现”I”与”A”不匹配。所以,”I”是”坏字符”。根据”坏字符规则”,此时模式串应该后移 2 –(-1)= 3 位。问题是,此时有没有更好的移法?

我们知道,此时存在”好后缀”。所以,可以采用“好后缀规则”:

后移位数 = 好后缀的位置 – 模式串中的上一次出现位置

计算时,位置的取值以”好后缀”的最后一个字符为准。如果”好后缀”在模式串中没有重复出现,则它的上一次出现位置为 -1。

所有的”好后缀”(MPLE、PLE、LE、E)之中,只有”E”在”EXAMPLE”之中出现两次,所以后移 6 – 0 = 6位。取坏规则和好规则的最大的那个值,也就是我们要后移6位。后移后如下图所示。

7.

继续从尾部开始比较,”P”与”E”不匹配,因此”P”是”坏字符”。根据”坏字符规则”,后移 6 – 4 = 2位。

8.

从尾部开始逐位比较,发现全部匹配,于是搜索结束。如果还要继续查找(即找出全部匹配),则根据”好后缀规则”,后移 6 – 0 = 6位,即头部的”E”移到尾部的”E”的位置。

更多的例子,点这里

四.算法详解

通过了一个例子后,相信大家对这个算法基本了解了,那么下面我们将通过具体实现来深入解释算法的一些细节。具体实现与上面那个例子稍微有点不一样,但原理是一样的。

首先我们要设计一个数组bmBc[],比如说bmBc[‘K’]表示坏字符‘k’在模式串中最右出现的位置距离模式串末尾的长度,那么当遇到坏字符的时候,模式串可以移动距离为: shift(坏字符) = bmBc[T[i]]-(m-1-i) (其中T[i]指的是在i位置上坏字符,(m-1-i)指的是坏字符位置到模式串末尾的长度),这个移动的距离与我们上面例子讨论的移动距离的方式虽然不一样,但原理是一样的,都是求坏字符位置与在模式串出现坏字符位置的距离,当然这个距离有可能是负的,但是没关系,遇到这种情况模式串就直接向后一位,重新开始匹配,但是由于有好后缀规则,我们选取大的进行移动,所以也可以不处理。如下图:

数组bmBc的创建非常简单,直接贴出伪代码代码如下:

  1. void preBmBc(char *x, int m, int bmBc[]) {
  2. int i;
  3. for (i = 0; i <ASIZE; ++i)
  4. bmBc[i] = m;
  5. for (i = 0; i < m - 1; ++i)
  6. bmBc[x[i]] = m - i - 1;
  7. }

上面ASIZE为该字符集词的个数,但是上述伪代码存在一个问题如果是像中文这样字符集的话会是bmBc数组非常大,所以我实现采用了如下的方法,就是通过键值对来取代数组,然后用一个专门的函数来查看键值对的值,如果存在就返回相应的值,不存在就返回模式串的长度。具体看代码

  1. private void preBmBc(String pattern,int patLength,Map<String,Integer> bmBc)
  2. {
  3. System.out.println("bmbc start process...");
  4. for(int i=patLength-2;i>=0;i--)
  5. {
  6. if(!bmBc.containsKey(String.valueOf(pattern.charAt(i))))
  7. {               bmBc.put(String.valueOf(pattern.charAt(i)),(Integer)(patLength-i-1));
  8. }
  9. }
  10. }
  11. private int getBmBc(String c,Map<String,Integer> bmBc,int m)
  12. {
  13. //如果在规则中则返回相应的值,否则返回pattern的长度
  14. if(bmBc.containsKey(c))
  15. {
  16. return  bmBc.get(c);
  17. }else
  18. {
  19. return m;
  20. }
  21. }

为了实现好后缀规则,需要定义一个数组suffix[],其中suffix[i] = s 表示以i为边界,与模式串后缀匹配的最大长度,如下图所示,用公式可以描述:满足P[i-s, i] == P[m-s, m]的最大长度s。

构建suffix数组的伪代码如下:

  1. suffix[m-1]=m;
  2. for (i=m-2;i>=0;--i){
  3. q=i;
  4. while(q>=0&&P[q]==P[m-1-i+q])
  5. --q;
  6. suffix[i]=i-q;
  7. }

有了suffix数组,就可以定义bmGs[]数组,bmGs[i] 表示遇到好后缀时,模式串应该移动的距离,其中i表示好后缀前面一个字符的位置(也就是坏字符的位置),构建bmGs数组分为三种情况,分别对应上述的移动模式串的三种情况

模式串中没有子串匹配上好后缀,但找不到一个最大前缀

模式串中有子串匹配上好后缀

模式串中没有子串匹配上好后缀,但找到一个最大前缀

构建bmGs数组的伪代码如下:

  1. void preBmGs(char *x, int m, int bmGs[]) {
  2. int i, j, suff[XSIZE];
  3. suffixes(x, m, suff);
  4. //模式串中没有子串匹配上好后缀,也找不到一个最大前缀
  5. for (i = 0; i < m; ++i)
  6. bmGs[i] = m;
  7. j = 0;
  8. //模式串中没有子串匹配上好后缀,但找到一个最大前缀
  9. for (i = m - 1; i >= 0; --i)
  10. if (suff[i] == i + 1)
  11. for (; j < m - 1 - i; ++j)
  12. if (bmGs[j] == m)
  13. bmGs[j] = m - 1 - i;
  14. //模式串中有子串匹配上好后缀
  15. for (i = 0; i <= m - 2; ++i)
  16. bmGs[m - 1 - suff[i]] = m - 1 - i;
  17. }

在计算完bmBc,BmGs数组后,BM算法伪代码实现如下:

  1. j = 0;
  2. while (j <= strlen(T) - strlen(P)) {
  3. for (i = strlen(P) - 1; i >= 0 && P[i] ==T[i + j]; --i)
  4. if (i < 0)
  5. match;
  6. else
  7. j += max(bmGs[i], bmBc[T[i]]-(m-1-i));
  8. }

BM算法一般情况下的算法复杂度为O(M/N),M为字符串长度,N为模式串长度。对于算法复杂度感兴趣的,点这里

参考文献:

JAVA实现源码

  1. import java.util.*;
  2. public class BoyerMoore {
  3. public static void main(String[] args) {
  4. // TODO Auto-generated method stub
  5. //      String text="HERE IS A SIMPLE EXAMPLE";
  6. //      String pattern="EXAMPLE";
  7. //      String pattern="GCAGAGAG";
  8. //      String text="WOWOWO!";
  9. //      String pattern="WOWO";
  10. String text="中国是一个伟大的国度;伟大的祖国啊";
  11. String pattern="伟大的国度";
  12. BoyerMoore bm=new BoyerMoore();
  13. bm.boyerMoore(pattern, text);
  14. }
  15. private void preBmBc(String pattern,int patLength,Map<String,Integer> bmBc)
  16. {
  17. System.out.println("bmbc start process...");
  18. for(int i=patLength-2;i>=0;i--)
  19. {
  20. if(!bmBc.containsKey(String.valueOf(pattern.charAt(i))))
  21. {
  22. bmBc.put(String.valueOf(pattern.charAt(i)),(Integer)(patLength-i-1));
  23. }
  24. }
  25. }
  26. private void suffix(String pattern,int patLength,int [] suffix)
  27. {
  28. suffix[patLength-1]=patLength;
  29. int q=0;
  30. for(int i=patLength-2;i>=0;i--)
  31. {
  32. q=i;
  33. while(q>=0&&pattern.charAt(q)==pattern.charAt(patLength-1-i+q))
  34. {
  35. q--;
  36. }
  37. suffix[i]=i-q;
  38. }
  39. }
  40. private void preBmGs(String pattern,int patLength,int []bmGs)
  41. {
  42. int i,j;
  43. int []suffix=new int[patLength];
  44. suffix(pattern,patLength,suffix);
  45. //模式串中没有子串匹配上好后缀,也找不到一个最大前缀
  46. for(i=0;i<patLength;i++)
  47. {
  48. bmGs[i]=patLength;
  49. }
  50. //模式串中没有子串匹配上好后缀,但找到一个最大前缀
  51. j=0;
  52. for(i=patLength-1;i>=0;i--)
  53. {
  54. if(suffix[i]==i+1)
  55. {
  56. for(;j<patLength-1-i;j++)
  57. {
  58. if(bmGs[j]==patLength)
  59. {
  60. bmGs[j]=patLength-1-i;
  61. }
  62. }
  63. }
  64. }
  65. //模式串中有子串匹配上好后缀
  66. for(i=0;i<patLength-1;i++)
  67. {
  68. bmGs[patLength-1-suffix[i]]=patLength-1-i;
  69. }
  70. System.out.print("bmGs:");
  71. for(i=0;i<patLength;i++)
  72. {
  73. System.out.print(bmGs[i]+",");
  74. }
  75. System.out.println();
  76. }
  77. private int getBmBc(String c,Map<String,Integer> bmBc,int m)
  78. {
  79. //如果在规则中则返回相应的值,否则返回pattern的长度
  80. if(bmBc.containsKey(c))
  81. {
  82. return  bmBc.get(c);
  83. }else
  84. {
  85. return m;
  86. }
  87. }
  88. public void  boyerMoore(String pattern,String text )
  89. {
  90. int m=pattern.length();
  91. int n=text.length();
  92. Map<String,Integer> bmBc=new HashMap<String,Integer>();
  93. int[] bmGs=new int[m];
  94. //proprocessing
  95. preBmBc(pattern,m,bmBc);
  96. preBmGs(pattern,m,bmGs);
  97. //searching
  98. int j=0;
  99. int i=0;
  100. int count=0;
  101. while(j<=n-m)
  102. {
  103. for(i=m-1;i>=0&&pattern.charAt(i)==text.charAt(i+j);i--)
  104. {   //用于计数
  105. count++;
  106. }
  107. if(i<0){
  108. System.out.println("one position is:"+j);
  109. j+=bmGs[0];
  110. }else{
  111. j+=Math.max(bmGs[i],getBmBc(String.valueOf(text.charAt(i+j)),bmBc,m)-m+1+i);
  112. }
  113. }
  114. System.out.println("count:"+count);
  115. }
  116. }

(转)Boyer-Moore算法的更多相关文章

  1. Boyer Moore算法(字符串匹配)

    上一篇文章,我介绍了KMP算法. 但是,它并不是效率最高的算法,实际采用并不多.各种文本编辑器的"查找"功能(Ctrl+F),大多采用Boyer-Moore算法. Boyer-Mo ...

  2. Boyer–Moore (BM)字符串搜索算法

    在计算机科学里,Boyer-Moore字符串搜索算法是一种非常高效的字符串搜索算法.它由Bob Boyer和J Strother Moore设计于1977年.此算法仅对搜索目标字符串(关键字)进行预处 ...

  3. Leetcode OJ : Implement strStr() [ Boyer–Moore string search algorithm ] python solution

    class Solution { public: int strStr(char *haystack, char *needle) { , skip[]; char *str = haystack, ...

  4. Boyer-Moore 字符串匹配算法

    字符串匹配问题的形式定义: 文本(Text)是一个长度为 n 的数组 T[1..n]: 模式(Pattern)是一个长度为 m 且 m≤n 的数组 P[1..m]: T 和 P 中的元素都属于有限的字 ...

  5. DPI (Deep Packet Inspection) 深度包检测技术

    详解DPI与网络回溯分析技术 随着网络通讯技术进步与发展,网络通讯已跨入大数据时代,如何监控各类业务系统的通讯数据在大数据流量中传输质量,以及针对海量的网络通讯数据的范畴中存在少量的恶意流量的检测,避 ...

  6. 【Java字符序列】Pattern

    简介 Pattern,正则表达式的编译表示,操作字符序列的利器. 整个Pattern是一个树形结构(对应于表达式中的‘|’),一般为链表结构,树(链表)的基本元素是Node结点,Node有各种各样的子 ...

  7. DPI深度报文检测架构及关键技术实现

    DPI深度报文检测架构及关键技术实现 当前DPI(Deep Packet Inspect深度报文识别)技术是安全领域的关键技术点之一,围绕DPI技术衍生出的安全产品类型也非常的多样.在分析DPI的进一 ...

  8. 2019-8-31-C#-对-byte-数组进行模式搜索

    title author date CreateTime categories C# 对 byte 数组进行模式搜索 lindexi 2019-08-31 16:55:58 +0800 2018-07 ...

  9. C# 对 byte 数组进行模式搜索

    本文告诉大家几个方法从 byte 数组找到对应的相同序列的数组 最简单的方法是进行数值判断,但是代码最少是使用Linq ,效率比较高是使用 Boyer-Moore 算法,下面就告诉大家几个算法的代码 ...

  10. 我熬夜读完这份“高分宝典”,竟4面拿下字节跳动offer

    前言 怎样的契机? 实际上,目前毕业已经两年时间了,在大学时就已经开始关注字节跳动的发展.一开始,我是电气自动化专业的,大二清楚目标之后就转计算机了,大四进了一家小型的互联网公司实习,具体就不说哪家了 ...

随机推荐

  1. JavaScript自学笔记(2)---function a(){} 和 var a = function(){}的区别(javascript)

    function a(){} 和 var a = function(){}的区别: 学习做浮窗,看到别人的代码里有: window.onresize = function(){ chroX = doc ...

  2. 一文看懂AI深度学习丨曼孚科技

    深度学习(Deep Learning)是机器学习的一种,而机器学习是实现人工智能的必经途径. 目前大部分表现优异的AI应用都使用了深度学习技术,引领了第三次人工智能的浪潮. 一. 深度学习的概念 深度 ...

  3. 英语语法 ( Spoken language )

    - - - -------------- 1,五个语序: 主语+谓语(中英语序一致)主语+系动词+表语 (中英语序一致)主语+谓语+宾语(中英语序一致)主语+谓语+间宾+直宾(中英语序一致)主语+谓语 ...

  4. Navicat Premium15安装与激活(破解)

    Navicat premium是一款数据库管理工具,是一个可多重连线资料库的管理工具,它可以让你以单一程式同时连线到 MySQL.SQLite.Oracle 及 PostgreSQL 资料库,让管理不 ...

  5. PHP0007:PHP基础-字符串

    php设置编码 用gbk编码识别utf8字符

  6. Avro介绍

    Avro介绍   Apache Avro是一个数据序列化系统. Avro所提供的属性: 1.丰富的数据结构2.使用快速的压缩二进制数据格式3.提供容器文件用于持久化数据4.远程过程调用RPC5.简单的 ...

  7. lasso-ridge

    线性回归 线性回归很简单,用线性函数拟合数据,用 mean square error (mse) 计算损失(cost),然后用梯度下降法找到一组使 mse 最小的权重. lasso 回归和岭回归(ri ...

  8. 【你不知道的javaScript 上卷 笔记7】javaScript中对象的[[Prototype]]机制

    [[Prototype]]机制 [[Prototype]]是对象内部的隐试属性,指向一个内部的链接,这个链接的作用是:如果在对象上没有找到需要的属性或者方法引用,引擎就 会继续在 [[Prototyp ...

  9. ubantu crontab定时任务设置

    Lynx浏览器安装.安装命令:sudo apt-get install lynx.打开终端输入:crontab -e若初次执行会出现以下(选择编译器,一般选4(Vim))Select an edito ...

  10. 题解 AT3718 【[ABC081B] Shift only】

    题目传送门 分析 直接暴力. 我们可以根据题意进行模拟,使用二重循环即可. 代码讲解 定义变量\(n\)和计数数组\(cnt\),再定义数组\(a\)并输入. int a[1000000]; int ...