一、BM算法介绍

  BM算法(Boyer-Moore算法)是罗伯特·波义尔(Robert Boyer)和杰·摩尔(J·Moore)在1977年共同提出的。与KMP算法不同的是,BM算法是模式串P由左向右移动,而字符的比较时由右向左进行。当文本字符与模式不匹配时,则根据预先定义好的“坏字符串偏移函数”和“好后缀偏移函数”计算出偏移量。它的简化版本BMH或整个算法通常在文本编辑器中用于“搜索”和“替代”命令。该算法从最右边的字符开始,从右到左扫描模式的字符。如果不匹配(或整个模式完全匹配),它将使用两个预先计算的函数将窗口向右移动。这两个移位函数称为后缀移位(也称为匹配移位)和字符移位(也称为出现移位)。

二、坏字符启发式

  这种方法称为坏字符启发法。 如果错误字符(即导致不匹配的文本符号)出现在模式中的其他位置,则也可以应用此功能。 然后可以移动模式,使其与该文本符号对齐。 下一个示例说明了这种情况。

Example:

0 1 2 3 4 5 6 7 8 9 ...
a b b a b a b a c b a
b a b a c            
    b a b a c        

  比较b-c导致不匹配。文本符号b出现在图案中的位置0和2处。可以移动该模式,以使模式中最右边的b与文本符号b对齐。

坏字符启发式预处理

  坏字符偏移是一个容量为σ的表,其中σ表示目标字符串中元素的种类数,每个字符对应的Ascall表中的十进制为下标。对于坏字符启发法,需要一个函数 ooc,该函数会针对字母表中的每个符号产生其在模式中最右边出现的位置;如果在模式中未出现该符号,则得出-1。

  令p = P∈A“P0 ... PM-1 (模式的)和 a∈A 一个字母符号。然后 ooc(p, a) = max{ j | pj = a },这里max(∅) = -1。

Example:

  • ooc(text, x) = 2

  • ooc(text, t) = 3

  字符串“文本”中符号“ x”最右出现在位置2。符号“ t”出现在位置0和3,最右边出现在位置3。

  某个模式p的出现函数存储在数组ooc中,该数组由字母符号索引。 对于每个符号A,相应的值ooc(p,a) 存储在 ooc[a]中。

 1     /**
2 * 计算字符在模式最后出现位置
3 */
4 private void bmInitocc() {
5 char a;
6 int j;
7 // 初始化所有字符出现为-1
8 for (a = 0; a < alphabetSize; a++) {
9 occ[a] = -1;
10 }
11 // 填充每种字符最后出现的位置
12 for (j = 0; j < m; j++) {
13 a = p[j];
14 occ[a] = j;
15 }
16 }

三、好后缀启发式

  有时,坏字符启发法会失败。 在以下情况下,比较a-b会导致不匹配。 模式符号a的最右出现与文本符号a的对齐将产生负向偏移。 相反,可能会移位1。 但是,在这种情况下,最好从图案的结构中得出最大可能的移位距离。 此方法称为好后缀试探法。

Example:

0 1 2 3 4 5 6 7 8 9 ...
a b a a b a b a c b a
c a b a b            
    c a b a b        

  后缀ab已匹配。可以移动模式,直到模式中下一个出现的ab对准文本符号ab,即位置2。

  在以下情况下,后缀ab已匹配。模式中没有其他出现ab的情况,因此可以将模式移到ab后面,即移至位置5。

Example:

0 1 2 3 4 5 6 7 8 9 ...
a b c a b a b a c b a
c b a a b            
          c b a a b  

  在以下情况下,后缀bab已匹配。 模式中没有其他出现bab的情况。 但是在这种情况下,模式不能像以前一样移至位置5,而只能移至位置3,因为模式(ab)的前缀与bab的结尾匹配。 我们将此情况称为良好后缀启发式的情况2。

Example:

0 1 2 3 4 5 6 7 8 9 ...
a a b a b a b a c b a
a b b a b            
      a b b a b      

  模式由坏字符和后缀启发式方法给出的两个距离中的最长的移位。

后缀启发式的预处理

  对于后缀启发式,使用数组 s。 如果在位置i-1处发生不匹配,即如果从位置i开始的模式后缀已匹配,则每个条目s [i]都包含模式的移位距离。 为了确定移动距离,必须考虑两种情况。

情况1:匹配的后缀出现在模式中的其他位置(图1)。

图1:匹配的后缀(灰色)出现在模式中的其他位置

情况2:只有部分匹配的后缀出现在模式的开头(图2)。

图2:匹配后缀的仅一部分出现在模式的开头

情况1:

  这种情况类似于Knuth-Morris-Pratt预处理。 匹配的后缀是模式后缀的边界。 因此,必须确定模式后缀的边界。 但是,现在需要在给定边界和具有该边界的模式的最短后缀之间进行逆映射。

  而且,必须使边界不能由同一字符向左扩展,因为这会在模式串移位后引起另一个不匹配。

  在预处理算法的以下第一部分中,将计算数组 f。 每个条目bpos [i]都包含模式后缀的最宽边界的起始位置,起始位置为i。 从位置m开始的后缀ε没有边界,因此f[m]设置为m + 1。

  与Knuth-Morris-Pratt(KMP)预处理算法相似,每个边界都是通过检查是否可以用同一字符向左扩展已知的较短边界来计算的。

  但是,边界不能向左扩展的情况也很有趣,因为如果发生不匹配,这会导致模式有希望的偏移。 因此,相应的移动距离保存在数组s中,前提是该条目尚未被占用。 后者是较短后缀具有相同边框的情况。

 1     /**
2 * 根据模式进行预处理(1/2)
3 */
4 private void bmPreprocess1() {
5 int i = m, j = m + 1;
6 f[i] = j;
7 while (i > 0) {
8 while (j <= m && p[i - 1] != p[j - 1]) {
9 if (s[j] == 0) s[j] = j - i;
10 j = f[j];
11 }
12 i--;
13 j--;
14 f[i] = j;
15 }
16 }

Example:

i: 0 1 2 3 4 5 6 7
p: a b b a b a b  
f: 5 6 4 5 6 7 7 8
s: 0 0 0 0 2 0 4 1

  从位置2开始的后缀babab的最宽边界是bab,从位置4开始。因此,f [2] =4。从位置5开始的后缀ab的最宽边界是ε,从位置7开始。因此,f [5] = 7。

数组s的值由不能向左扩展的边界确定。

  从位置2开始的后缀babab具有从位置4开始的边界bab。由于p [1]≠p [3],因此该边界无法向左扩展。 差异4 – 2 = 2是bab匹配时的移位距离,然后发生不匹配。 因此,s [4] = 2。

  从位置2开始的后缀babab也具有从位置6开始的边界b。此边界也不能扩展。 如果b匹配,则不匹配6 – 2 = 4是移位距离。 因此,s [6] = 4。

  从位置6开始的后缀b具有从位置7开始的边界ε。该边界不能向左扩展。 如果没有匹配,即在第一次比较中发生不匹配,则7 – 6 = 1的差就是移动距离。 因此,s [7] = 1。

  注:所谓边界就是模式P中从i开始的后缀中,其后缀中的前缀和后缀所匹配的最大公共子串开始的位置,也就是说 f [i] 到 m-1 的那一串 和 i开始往后的那段是一样的。ε是所有前后缀的匹配结果,相当于集合中的空集,没有边界。

情况2:

  在这种情况下,模式的匹配后缀的一部分出现在模式的开头。 这意味着该部分是模式的边界。 可以将模式移动到其最大匹配边界允许的范围内(图2)。

  在情况2的预处理中,对于每个后缀,确定该后缀中包含的模式的最宽边界。

  模式最宽边界的起始位置完全存储在f [0]中。 在上面的示例中,该值是5,因为边界ab从位置5开始。

  在以下预处理算法中,该值f [0]最初存储在数组s 的所有空闲条目中。 但是,当模式的后缀变得比f [0]短时,算法会继续使用模式的下一个更宽的边界,即f [j]。

 1     /**
2 * 根据模式进行预处理(2/2)
3 */
4 private void bmPreprocess2() {
5 int i, j;
6 j = f[0];
7 for (i = 0; i <= m; i++) {
8 if (s[i] == 0) s[i] = j;
9 if (i == j) j = f[j];
10 }
11 }

Example:

i: 0 1 2 3 4 5 6 7
p: a b b a b a b  
f: 5 6 4 5 6 7 7 8
s: 5 5 5 5 2 5 4 1

四、分析

  如果文本中模式的匹配项数量恒定,则在最坏的情况下,Boyer-Moore搜索算法会执行O(n)比较。 证明这一点相当困难。通常,Θ(n·m)比较是必要的,例如 如果模式是am,则文本是an。 通过稍微修改算法,即使在一般情况下,比较次数也可以限制为O(n)。如果字母表与模式的长度相比较大,则该算法将对平均值进行 O(n / m)比较。 这是因为由于坏字符启发法,通常有可能将m移位。

五、总结

  在遇到不匹配的情况下,Boyer-Moore算法使用两种不同的启发式方法来确定最大可能的移位距离:“坏字符”和“好后缀”启发式方法。 两种启发式方法都可以导致m的移位距离。 如果第一次比较导致不匹配,并且模式中根本没有出现相应的文本符号,那么对于坏字符启发式就是这种情况。 对于好后缀启发式法,如果只有第一个比较是匹配项,但该字符不在模式中的其他位置出现,则为这种情况。

  好后缀启发式算法的预处理相当难以理解和实现。 因此,有时会找到Boyer-Moore算法的版本,在该版本中,好后缀启发式法被遗弃了。 有观点认为,坏字符启发法就足够了,而好后缀启发法不会节省很多比较。 但是,对于字符数量小的字母串而言,情况并非如此。

源代码:

  1 package algorithm;
2
3 public class BM {
4 private static String name = "Boyer-Moore";
5 private static int alphabetSize = 256;
6 private char[] p, t; // 模式,文本
7 private int m, n; // 模式的长度,文本的长度
8 private int[] occ; // 记录文本字符在模式中的位置
9 private String matches; // 匹配位置
10 private char[] showmatches; // 显示匹配的字符数组
11 private int[] f;
12 private int[] s;
13
14 public BM() {
15 occ = new int[alphabetSize];
16 }
17
18 public void search(String tt, String pp) {
19 setText(tt);
20 setPatten(pp);
21 bmSearch();
22 }
23
24 /**
25 * 设置文本
26 *
27 * @param tt
28 */
29 private void setText(String tt) {
30 n = tt.length();
31 t = tt.toCharArray();
32 initMatches();
33 }
34
35 /**
36 * 设置模式
37 *
38 * @param pp
39 */
40 private void setPatten(String pp) {
41 m = pp.length();
42 p = pp.toCharArray();
43 f = new int[m + 1];
44 s = new int[m + 1];
45 bmPreprocess();
46 }
47
48 /**
49 * 计算字符在模式最后出现位置
50 */
51 private void bmInitocc() {
52 char a;
53 int j;
54 for (a = 0; a < alphabetSize; a++) {
55 occ[a] = -1;
56 }
57 for (j = 0; j < m; j++) {
58 a = p[j];
59 occ[a] = j;
60 }
61 }
62
63 /**
64 * 根据模式进行预处理(1/2)
65 */
66 private void bmPreprocess1() {
67 int i = m, j = m + 1;
68 f[i] = j;
69 while (i > 0) {
70 while (j <= m && p[i - 1] != p[j - 1]) {
71 if (s[j] == 0) s[j] = j - i;
72 j = f[j];
73 }
74 i--;
75 j--;
76 f[i] = j;
77 }
78 }
79
80 /**
81 * 根据模式进行预处理(2/2)
82 */
83 private void bmPreprocess2() {
84 int i, j;
85 j = f[0];
86 for (i = 0; i <= m; i++) {
87 if (s[i] == 0) s[i] = j;
88 if (i == j) j = f[j];
89 }
90 }
91
92 /**
93 * 根据模式进行预处理
94 */
95 private void bmPreprocess() {
96 bmInitocc();
97 bmPreprocess1();
98 bmPreprocess2();
99 }
100
101 /**
102 * 初始化匹配位置该显示的数组
103 */
104 private void initMatches() {
105 matches = "";
106 showmatches = new char[n];
107 for (int i = 0; i < n; i++) {
108 showmatches[i] = ' ';
109 }
110 }
111
112 /**
113 * 在文本中搜索所有出现的模式
114 */
115 private void bmSearch() {
116 int i = 0, j;
117 while (i <= n - m) {
118 j = m - 1;
119 while (j >= 0 && p[j] == t[i + j])
120 j--;
121 if (j < 0) {
122 report(i);
123 i += s[0];
124 } else {
125 i += Math.max(s[j + 1], j - occ[t[i + j]]);
126 }
127 }
128 }
129
130 /**
131 * 匹配报告
132 *
133 * @param i
134 */
135 private void report(int i) {
136 matches += i + " ";
137 showmatches[i] = '^';
138 }
139
140 /**
141 * 搜索后返回匹配位置
142 *
143 * @return
144 */
145 public String getMatches() {
146 return matches;
147 }
148
149 /**
150 * 测试主函数
151 *
152 * @param args
153 */
154 public static void main(String[] args) {
155 BM bm = new BM();
156 System.out.println(name);
157 String tt, pp;
158 tt = "abcdabcd";
159 pp = "abc";
160 bm.search(tt, pp);
161 System.out.println(pp);
162 System.out.println(tt);
163 System.out.println(bm.showmatches);
164 System.out.println(bm.getMatches());
165 tt = "abababaa";
166 pp = "aba";
167 bm.search(tt, pp);
168 System.out.println(pp);
169 System.out.println(tt);
170 System.out.println(bm.showmatches);
171 System.out.println(bm.getMatches());
172 }
173 }

参考:

String Matching - Boyer-Moore algorithm

字符串与模式匹配算法(四):BM算法的更多相关文章

  1. 字符串匹配算法之BM算法

    BM算法,全称是Boyer-Moore算法,1977年,德克萨斯大学的Robert S. Boyer教授和J Strother Moore教授发明了一种新的字符串匹配算法. BM算法定义了两个规则: ...

  2. 常用算法3 - 字符串查找/模式匹配算法(BF & KMP算法)

    相信我们都有在linux下查找文本内容的经历,比如当我们使用vim查找文本文件中的某个字或者某段话时,Linux很快做出反应并给出相应结果,特别方便快捷! 那么,我们有木有想过linux是如何在浩如烟 ...

  3. 字符串匹配算法(二)-BM算法详解

    我们在字符串匹配算法(一)学习了BF算法和RK算法,那有没更加高效的字符串匹配算法呢.我们今天就来聊一聊BM算法. BM算法 我们把模式串和主串的匹配过程,可以看做是固定主串,然后模式串不断在往后滑动 ...

  4. 字符串与模式匹配算法(二):MP算法

    一.MP算法介绍 MP 算法(Morris-Pratt算法)是一种快速串匹配算法,它是詹姆斯·莫里斯(James Morris)和沃恩·普莱特(Vaughan Pratt)在1970年提出的一种快速匹 ...

  5. 字符串与模式匹配算法(五):BMH算法

    一.BMH算法介绍 在BM算法的实际应用中,坏字符偏移函数的应用次数要远远超过好后缀偏移函数的应用次数,坏字符偏移函数在匹配过程中起着移动指针的主导作用.在实际匹配过程,只是用坏字符偏移函数也非常有效 ...

  6. 字符串与模式匹配算法(一):BF算法

    一.BF算法的基本思想 BF(Brute Force)算法是模式匹配中最简单.最直观的算法.该算法最基本的思想是从主串的第 start 个字符起和模式P(要检索的子串)的第1个字符比较,如果相等,则逐 ...

  7. 字符串的模式匹配算法——KMP模式匹配算法

    朴素的模式匹配算法(C++) 朴素的模式匹配算法,暴力,容易理解 #include<iostream> using namespace std; int main() { string m ...

  8. 字符串与模式匹配算法(六):Needleman–Wunsch算法

    一.Needleman-Wunsch 算法 尼德曼-翁施算法(英语:Needleman-Wunsch Algorithm)是基于生物信息学的知识来匹配蛋白序列或者DNA序列的算法.这是将动态算法应用于 ...

  9. 字符串模式匹配算法2 - AC算法

    上篇文章(http://www.cnblogs.com/zzqcn/p/3508442.html)里提到的BF和KMP算法都是单模式串匹配算法,也就是说,模式串只有一个.当需要在字符串中搜索多个关键字 ...

随机推荐

  1. Mybatis简单查询

    目录 前言 一.时间区间查询 1.xml中实现 2. Mybatis Plus 方式 二.模糊查询 1.xml中实现 2. Mybatis Plus 方式 前言  好记性不如烂笔头,记录内容,方便日后 ...

  2. scrum项目冲刺_day02总结

    摘要:今日完成任务. 1.appUI页面完成 2.图像识别正在进行 总任务: 一.appUI页面(已完成) 二.首页功能: 1.图像识别功能 2.语音识别功能 3.垃圾搜索功能 4.相关新闻爬取 三. ...

  3. 解决sofaboot项目右键入口方法没有run sofa application

    选中入口方法名,右键出现run sofa application

  4. TypeScript与面向对象

    目录 1.引 2.类(class) 3.构造函数和this 4.继承 5.super 6.抽象类 7.接口 8.属性的封装 9.泛型 1.引 简而言之就是程序之中所有的操作都需要通过对象来完成.一切操 ...

  5. vue实现事件代理(通过事件冒泡实现)

    事件代理/事件委托以ul>li来模拟 使用冒泡的用法:使用冒泡的用法来实现事件代理 分离出来 动态根据索引添加类名:

  6. win10系统安装jdk

    应用程序形式 文件包形式 环境变量配置 https://www.cnblogs.com/gz9218/p/2ae083861f6749899b1b800faabe1c17.html

  7. 获取用户id的方法

    /** 获取ip */function getip() { if(getenv("HTTP_X_FORWARDED_FOR")!=''){ $cip = getenv(" ...

  8. Windows命令行在任意位置启动和退出nginx

    写在前面 本文给出Windows系统中能在任意路径下通过命令行启动和退出nginx的方法.不想看过程的读者可以直接跳转到结论,一样能解决问题. 正文 过程 很多Windows下的nginx教程都教我们 ...

  9. 定要过python二级 第一套

    1.和强类型语言相对应的是弱类型语言,Python.JavaScript.PHP 等脚本语言一般都是弱类型的.弱类型语言有两个特点: 变量无须声明就可以直接赋值,对一个不存在的变量赋值就相当于定义了一 ...

  10. 实时渲染基础(4)纹理(Texture)

    目录 纹理映射(Texture Mapping) 球形贴图(Spherical Map) 立方体贴图(Cube Map) 纹理走样问题 Mipmap 各向异性过滤(Ripmap) 纹理应用技术(Tex ...