不用找了,学习BM算法,这篇就够了(思路+详注代码)
写在前面
在计算机科学里,Boyer-Moore字符串搜索算法是一种非常高效的字符串搜索算法。它由Bob Boyer和J Strother Moore设计于1977年。此算法仅对搜索目标字符串(关键字)进行预处理,而非被搜索的字符串。虽然Boyer-Moore算法的执行时间同样线性依赖于被搜索字符串的大小,但是通常仅为其它算法的一小部分:它不需要对被搜索的字符串中的字符进行逐一比较,而会跳过其中某些部分。通常搜索关键字越长,算法速度越快。它的效率来自于这样的事实:对于每一次失败的匹配尝试,算法都能够使用这些信息来排除尽可能多的无法匹配的位置。
在学习研究BM算法之前,我是已经掌握了KMP算法,所以建议还没有掌握的同学,先去学习一下,循序渐进的来,可以看我的KMP算法的文章。为什么说循序渐进,是因为BM算法,在大多数情况下,表现的比KMP算法优秀,所以大部分时候,都当做KMP进阶的算法来学习。BM算法从模式串的尾部开始匹配,且拥有在最坏情况下 $O(N) $的时间复杂度。有数据表明,在实践中,比 KMP 算法的实际效能高,可以快大概 3-5 倍,很值得学习。在学习BM算法的时候,找了很多资料,也遇到了很多优秀的文章,不过目前还没有碰到即讲清楚了原理,又实现了代码的文章,java版的更是不容易找。所以我这里打算站在大神的肩膀上学习,写下这篇文章,由于一些图画的比较占用时间,我就直接引用一些博文中的图片。感兴趣的可以直接看大神们的文章。
- 阮一峰的BM算法文章
- BM算法的论文
- BM算法创造者的例子
- BM算法讲解中C语言讲解的比较好的文章
- 图解BM算法
- Boyer-Moore algorithm
BM算法原理
BM算法定义了两个规则:
- 坏字符规则:当文本串中的某个字符跟模式串的某个字符不匹配时,我们称文本串中的这个失配字符为坏字符,此时模式串需要向右移动,
移动的位数 = 坏字符在模式串中的位置 - 坏字符在模式串中最右出现的位置
。此外,如果"坏字符"不包含在模式串之中,则最右出现位置为-1
。 - 好后缀规则:当字符失配时,
后移位数 = 好后缀在模式串中的位置 - 好后缀在模式串上一次出现的位置
,且如果好后缀在模式串中没有再次出现,则为-1
。
下面举例说明BM算法。例如,给定文本串“HERE IS A SIMPLE EXAMPLE
”,和模式串“EXAMPLE
”,现要查找模式串是否在文本串中,如果存在,返回模式串在文本串中的位置。
- 首先,“文本串"与"模式串"头部对齐,从尾部开始比较。”
S
“与”E
“不匹配。这时,”S
“就被称为"坏字符”(bad character),即不匹配的字符,它对应着模式串的第6
位。且"S
“不包含在模式串”EXAMPLE
“之中(相当于最右出现位置是-1
),这意味着可以把模式串后移6-(-1)=7
位,从而直接移到”S
"的后一位。
- 依然从尾部开始比较,发现"
P
“与”E
“不匹配,所以”P
“是"坏字符”。但是,"P
“包含在模式串”EXAMPLE
"之中。因为“P
”这个“坏字符”对应着模式串的第6
位(从0
开始编号),且在模式串中的最右出现位置为4
,所以,将模式串后移6-4=2
位,两个"P"对齐。
- 依次比较,得到 “
MPLE
”匹配,称为"好后缀"(good suffix),即所有尾部匹配的字符串。注意,"MPLE
"、"PLE
"、"LE
"、"E
"都是好后缀。
- 发现“
I
”与“A
”不匹配:“I
”是坏字符。如果是根据坏字符规则,此时模式串应该后移2-(-1)=3
位。问题是,有没有更优的移法?
- 更优的移法是利用好后缀规则:当字符失配时,
后移位数 = 好后缀在模式串中的位置 - 好后缀在模式串中上一次出现的位置
,且如果好后缀在模式串中没有再次出现,则为-1
。所有的“好后缀”(MPLE、PLE、LE、E
)之中,只有“E
”在“EXAMPLE
”的头部出现,所以后移6-0=6
位。可以看出,“坏字符规则”只能移3
位,“好后缀规则”可以移6
位。每次后移这两个规则之中的较大值。这两个规则的移动位数,只与模式串有关,与原文本串无关。
- 继续从尾部开始比较,“P”与“E”不匹配,因此“P”是“坏字符”,根据“坏字符规则”,后移 6 - 4 = 2位。因为是最后一位就失配,尚未获得好后缀。
好后缀加深理解
由上可知,BM算法不仅效率高,而且构思巧妙,容易理解。坏字符规则相对而言比较好理解,好后缀如果还不理解,我这里再继续举个例子解释一下,这里加深理解。
- 如果模式串中存在已经匹配成功的好后缀,则把目标串与好后缀对齐,然后从模式串的最尾元素开始往前匹配。
- 如果无法找到匹配好的后缀,找一个匹配的最长的前缀,让目标串与最长的前缀对齐(如果这个前缀存在的话)。模式串[m-s,m] = 模式串[0,s] 。
- 如果完全不存在和好后缀匹配的子串,则右移整个模式串。
先实现好字符规则
BM算法还是很好理解的,其实如果你之前学习KMP算法你也会有同样的感受,KMP算法理解起来不是很难,但是重点在于怎么去实现next数组。BM算法也是,原理理解起来其实非常的容易,不过怎么去实现,没有一套标准的代码。不过可以研究别人的代码,然后实现一套尽量适合精简的代码。还是一样,一步一步来,我们先来实现好字符规则。好字符规则的代码如下,我会在代码中必要的地方加入注释,辅助理解,代码是最好的老师。
public static void getRight(String pat, int[] right) {//首先创建一个模式串的字符位置的数组,初始化为-1,就是用于记录模式串//中,每个字符在模式串中的相对位置,这里直接用的是256,也//就是ASCII码的最大值,当然,如果你的字符串中只限制了26个//字符,你也可以直接使用26for (int i = 0; i < 256; i++) {right[i] = -1;}//值得一提的是,通过这种方式,可以你会发现,如果模式串中存在相同的//字符,那么right数组中,记录的是最右的那个字符的位置for (int j = 0; j < pat.length(); j++) {right[pat.charAt(j)] = j;}
}public static int Search(String txt, String pat, int[] right) {int M = txt.length();//主串的长度int N = pat.length();//模式串的长度int skip;//用于记录跳过几个字符for (int i = 0; i < M - N; i += skip) {skip = 0;//每次进入循环要记得初始化为0for (int j = N - 1; j >= 0; j--) {//不相等,意味着出现坏字符,按照上面的规则移动if (pat.charAt(j) != txt.charAt(i + j)) {skip = j - right[txt.charAt(i + j)];//skip之所以会小于1,可能是因为坏字符在模式串中最右的位置,可能//在j指向字符的右侧,就是已经越过了。if (skip < 1) skip = 1;break;}}//注意了这个时候循环了一遍之后,skip如果等于0,意味着没有坏字符出现,所以//匹配成功,返回当前字符i的位置if (skip == 0)return i;}return -1;
}
完整BM实现
上面的代码不难理解,相信你已经看懂了,那么接下来也不用单独来讲好后缀的实现,直接上完整的实现代码。因为完整的BM实现中,就是比较坏字符规则以及好后缀规则,哪个移动的字符数更多,就使用哪个。老样子,下面的代码中我尽量的加注释。
public static int pattern(String pattern, String target) {int tLen = target.length();//主串的长度int pLen = pattern.length();//模式串的长度//如果模式串比主串长,没有可比性,直接返回-1if (pLen > tLen) {return -1;}int[] bad_table = build_bad_table(pattern);// 获得坏字符数值的数组,实现看下面int[] good_table = build_good_table(pattern);// 获得好后缀数值的数组,实现看下面for (int i = pLen - 1, j; i < tLen;) {System.out.println("跳跃位置:" + i);//这里和上面实现坏字符的时候不一样的地方,我们之前提前求出坏字符以及好后缀//对应的数值数组,所以,我们只要在一边循环中进行比较。还要说明的一点是,这里//没有使用skip记录跳过的位置,直接针对主串中移动的指针i进行移动for (j = pLen - 1; target.charAt(i) == pattern.charAt(j); i--, j--) {if (j == 0) {//指向模式串的首字符,说明匹配成功,直接返回就可以了System.out.println("匹配成功,位置:" + i);//如果你还要匹配不止一个模式串,那么这里直接跳出这个循环,并且让i++//因为不能直接跳过整个已经匹配的字符串,这样的话可能会丢失匹配。
// i++; // 多次匹配
// break;return i;}}//如果出现坏字符,那么这个时候比较坏字符以及好后缀的数组,哪个大用哪个i += Math.max(good_table[pLen - j - 1], bad_table[target.charAt(i)]);}return -1;
}//字符信息表
public static int[] build_bad_table(String pattern) {final int table_size = 256;//上面已经解释过了,字符的种类int[] bad_table = new int[table_size];//创建一个数组,用来记录坏字符出现时,应该跳过的字符数int pLen = pattern.length();//模式串的长度for (int i = 0; i < bad_table.length; i++) {bad_table[i] = pLen; //默认初始化全部为匹配字符串长度,因为当主串中的坏字符在模式串中没有出//现时,直接跳过整个模式串的长度就可以了}for (int i = 0; i < pLen - 1; i++) {int k = pattern.charAt(i);//记录下当前的字符ASCII码值//这里其实很值得思考一下,bad_table就不多说了,是根据字符的ASCII值存储//坏字符出现最右的位置,这在上面实现坏字符的时候也说过了。不过你仔细思考//一下,为什么这里存的坏字符数值,是最右的那个坏字符相对于模式串最后一个//字符的位置?为什么?首先你要理解i的含义,这个i不是在这里的i,而是在上面//那个pattern函数的循环的那个i,为了方便我们称呼为I,这个I很神奇,虽然I是//在主串上的指针,但是由于在循环中没有使用skip来记录,直接使用I随着j匹配//进行移动,也就意味着,在某种意义上,I也可以直接定位到模式串的相对位置,//理解了这一点,就好理解在本循环中,i的行为了。//其实仔细去想一想,我们分情况来思考,如果模式串的最//后一个字符,也就是匹配开始的第一个字符,出现了坏字符,那么这个时候,直//接移动这个数值,那么正好能让最右的那个字符正对坏字符。那么如果不是第一个//字符出现坏字符呢?这种情况你仔细想一想,这种情况也就意味着出现了好后缀的//情况,假设我们将最右的字符正对坏字符bad_table[k] = pLen - 1 - i;}return bad_table;
}//匹配偏移表
public static int[] build_good_table(String pattern) {int pLen = pattern.length();//模式串长度int[] good_table = new int[pLen];//创建一个数组,存好后缀数值//用于记录最新前缀的相对位置,初始化为模式串长度,因为意思就是当前后缀字符串为空//要明白lastPrefixPosition 的含义int lastPrefixPosition = pLen;for (int i = pLen - 1; i >= 0; --i) {if (isPrefix(pattern, i + 1)) {//如果当前的位置存在前缀匹配,那么记录当前位置lastPrefixPosition = i + 1;}good_table[pLen - 1 - i] = lastPrefixPosition - i + pLen - 1;}for (int i = 0; i < pLen - 1; ++i) {//计算出指定位置匹配的后缀的字符串长度int slen = suffixLength(pattern, i);good_table[slen] = pLen - 1 - i + slen;}return good_table;
}//前缀匹配
private static boolean isPrefix(String pattern, int p) {int patternLength = pattern.length();//模式串长度//这里j从模式串第一个字符开始,i从指定的字符位置开始,通过循环判断当前指定的位置p//之后的字符串是否匹配模式串前缀for (int i = p, j = 0; i < patternLength; ++i, ++j) {if (pattern.charAt(i) != pattern.charAt(j)) {return false;}}return true;
}//后缀匹配
private static int suffixLength(String pattern, int p) {int pLen = pattern.length();int len = 0;for (int i = p, j = pLen - 1; i >= 0 && pattern.charAt(i) == pattern.charAt(j); i--, j--) {len += 1;}return len;
}
理解一下上面代码,这里我针对上面代码举个例子,计算之后的两张表的数值如下:
总结
其实如果你把上面理解了,我相信你会有一种兴奋,可能还会有一种疑问,就是BM的算法,如果将坏字符和好后缀规则都实现了,看着代码量怎么多,而且在计算两个位移数组的时候,相当于要做这么多准备工作,难道不会影响效率么。这个问题我也不好回答,根据不同语言的具体实现,代码的执行效率也会有所影响,不过有一点可以肯定的是,当字符匹配的长度量非常大的时候,BM的算法优势还是很明显的,而且要知道,BM算法在CV中应用的还是很广泛的,毕竟CV中的数据集样本动不动就是上百万。BM算法非常值得一学。
不用找了,学习BM算法,这篇就够了(思路+详注代码)相关推荐
- caffe-源码学习——只看一篇就够了
caffe-源码学习--只看一篇就够了 网络模型 说caffe代码难懂,其实关键点在于caffe中有很多基础的数学运算代码,如果能够对掌握这些数学运算,剩下的就是推公式了. 激活函数 sigmoid ...
- 学习MyBatis3这一篇就够了
目录 第一章 MyBatis3概述 1.1.概述 1.2.特点 1.3.对比 1.4.官网 1.5.下载 第二章 MyBatis3的增删改查 2.1.环境准备 2.2.创建工程 2.3.导入依赖 2. ...
- 学习javascript这一篇就够了超详细笔记(建议收藏)上
学习javascript这一篇就够了超详细笔记(建议收藏)上 1.初识 计算机基础导读 编程语言 计算机基础 初识js 浏览器执行 js组成 js初体验-三种书写位置 js注释 js输入输出语句 2. ...
- 强化学习入门这一篇就够了!!!万字长文
强化学习 强化学习入门这一篇就够了万字长文带你明明白白学习强化学习... 强化学习入门这一篇就够了 强化学习 前言 一.概率统计知识回顾 1.1 随机变量和观测值 1.2 概率密度函数 1.3 期望 ...
- 学习FastDFS这一篇就够了
目录 第一章 FastDFS简介 1.1.FastDFS的简介 1.2.FastDFS的发展历史 1.3.FastDFS的整体架构 1.4.FastDFS的使用用户 1.5.FastDFS的官方网址 ...
- 学习Redis这一篇就够了
这里写目录标题 本文脑图 redis基本数据结构 本文脑图 前言 Redis核心对象 String类型 int SDS SDS与c语言字符串对比 String类型应用 Hash类型 字典 rehash ...
- 干货!学习 Python 看这篇管够!!!
文 | 潮汐 来源:Python 技术「ID: pythonall」 写在前面 各位朋友们大家好,时间飞逝,转眼咱们公众号运营 2 年了,这两年感谢各位忠实粉丝的陪伴,让我们能更有动力继续前行,也希望 ...
- 学习SpringSecurity这一篇就够了
目录 一.SpringSecurity 框架简介 1.1.概要 1.2.Spring Security到底能干什么? 1.3.常用术语 1.4.历史 1.5.同款产品对比 1.6.模块划分 二.Spr ...
- 学习 Python3 这一篇就够了
Java工程师的进阶之路 目录 配置篇 一.环境安装 二.pip的使用 三.运行程序 基础篇 一.变量,标识符,类型转换 1.1.变量 1.2.标识符 1.3.类型转换 二.运算符 2.1.基础运算符 ...
最新文章
- Day2_and_Day3 文件操作
- 推荐两个Firefox插件
- 偏方使用不当担心被毁容! - 生活至上,美容至尚!
- Search For Mafuyu dfs,树的遍历,期望(济南)
- 详测 Generics Collections TQueue (3): OnNotify、Extract
- 多重选择函数c语言,大佬在吗,我用C写了一个去多重括号的函数,结果。。。...
- java实现多线程抢单_JAVA实现多线程的四种方式
- C语言 游戏远程call调用,创建远程线程 调用游戏所有call
- 使用html+css仿搜狐网址页面布局
- Android常见界面控件(基础入门)
- c语言查看错误步骤命令,C语言之预处理命令
- ZZNUOJ_用C语言编写程序实现1236:数的逆转(附完整源码)
- POJ1555-Polynomial Showdown
- Opencv中的数据存储(1)
- javascript getDay()方法 语法
- Win11如何清除最近打开过的文件记录?
- 如何解除病毒对各种杀毒软件的劫持
- Echarts中折线图Y轴数据值太长显示不全-解决办法
- 大数据培训:生活中这些场景都用到了大数据
- web开发指南_成为专业Web开发人员的实用指南