工业级字符串匹配算法:BM算法原理分析和代码实现
在文本编辑器中,如 Word,记事本等,利用 Ctrl+F 快捷键来实现文本的查找或替换,显然,这里也用到了字符串匹配算法,但是通常情况下查找的文本对象(即主串)数据量很大,因此我们不能采用BF算法或者 RK 算法来进行匹配,需要一个更加高效的算法,并且极端情况下效率依旧不会退化很多的算法。
RK 算法与 BF 算法:https://blog.csdn.net/m0_37264516/article/details/84836874
BM 算法核心思想概述
如下图,我们在进行字符串匹配的时候,拿模式串去匹配,匹配失败则往后挪一个单位。
若要提高匹配效率,考虑是否存在一种规律,在匹配的时候,一旦匹配失败,可以根据规律多往后挪几位,即排除 100% 不能匹配的情况。
BM 算法就是通过寻找这个规律,来提高匹配效率的。
BM算法原理分析
BM 算法所找到的规律分为两个部分,一个是坏字符规则,一个是好后缀规则
通过坏字符规则和好后缀规则分别计算出对应的模式串后移的位数,取最大值作为真正后移的位数。
接下来我们先来看坏字符规则是如何得到模式串移动的位数的。
BM 算法中,每次匹配时并不是从左往右匹配,而是倒过来从模式串末尾开始向前匹配,如图:
在匹配过程中,第一个遇到的匹配失败的字符叫做坏字符,上图中,为 ① 处,即
b
①拿字符
b在模式串中寻找最后一次出现的位置,发现前一个就是
b,因此字符串往后移 1 位,再次从后往前匹配。
理解坏字符规则的原理后,我们再来考虑一下当匹配到坏字符时,如何计算模式串滑动的位数呢?
我们假设坏字符对应模式串中的下标为
x,在模式串中寻找到的坏字符的位置为
y,当找不到时
y为-1,则滑动位数为
x-y,如图:
坏字符规则注意要点:
- 在模式串中寻找坏字符时,可能出现滑动位数为负数的情况,如图:
- 在模式串中寻找坏字符时,为了提高效率,我们将模式串所有字符和它的下标存储在一个散列表中,同一个字符的话,存靠后位置的字符。
由于上述第一点的情况,我们需要引入好后缀规则来避免坏字符规则的弊端。
好后缀的规则相对而言比较复杂一些,首先当我们遇到坏字符时,坏字符后面匹配成功的后缀称为好后缀,再从模式串寻找是否存在与好后缀匹配的子串,若存在,则直接滑动到所匹配的子串处,如图:
若模式串中没有找到好后缀,我们就可以简单的将模式串直接移到最后吗(如图)?
可以发现,虽然没有在模式串中找到与好后缀匹配的子串,但是模式串第一个字符和好后缀的最后一个字符是匹配的,如图中的情况,若不进行判断很容易导致过度滑动,错过能够匹配成功的情况。
针对过度滑动的情况,如何处理呢?
在模式串中没有找到好后缀时,应该再进行判断是否存在模式串前缀子串和好后缀的后缀子串是否存在匹配的情况,例如上图中,好后缀是
bc,
bc的后缀子串
c与模式串前缀子串
c匹配,因此滑动
6位而不是
7位。
至此,我们已经了解了坏字符规则和好后缀规则的原理,当字符串匹配时,分别计算两个规则的滑动位数,取最大值。接下来我们来思考一下,如何实现上述这些原理呢?
算法实现
首先我们来看看如何实现坏字符串规则,原理部分也说了,在寻找模式串中的坏字符位置时,可以采用散列表以提高效率。
假设字符串的字符集不是很大,我们用一个大小为 256 的数组保存每个字符的位置,数组下标为字符的 ASCII 码值,代码如下:
private static final int SIZE = 256; private void generateIndex(char [] str, int m, int index[]){ for(int i=0; i<SIZE; i++){ index[i] = -1;//初始化散列表 } for(int i=0; i<m; i++){ int ascii = (int)str[i]; index[ascii] = i;//遇到相同字符存储的是靠后的字符 } }
在这里我们先将坏字符规则先写好:
public int bm(char [] mainStr, int n, char [] str, int m){ int [] index = new int[SIZE]; generateIndex(str, m, index);//记录模式串每个字符最后一次出现的位置 int i = 0;//表示模式串与主串匹配时模式串第一位对应主串的下标 while(i <= n - m){ int j = m - 1; for( ; j >= 0; j--){//从后往前匹配 if(mainStr[i+j] != str[j]) break;//遇到坏字符串 } if(j < 0){ return i;//匹配成功,返回主串中匹配成功的第一个字符的位置 } int ascii = (int)mainStr[i+j];//坏字符串ASCII值 i = i + (j - index[ascii]);//滑动 } return -1; }
接下来再来考虑好后缀规则实现方案。
好后缀一共分两个步骤:
- 在模式串中查找另一个与好后缀匹配的子串,若有多个,取靠后的
- 若不存在与好后缀匹配的子串,则查找模式串前缀子串与好后缀的后缀子串匹配的最长子串
上述两步操作,都可以使用暴力匹配的方式去做,但是为了 BM 算法的效率,我们需要采用更高效的做法。
由于好后缀也是模式串的后缀子串,因此我们可以将模式串进行预处理,预先计算好所有模式串的后缀子串对应的另一个可匹配子串的位置。
一个字符串的所有后缀子串,实际上只有长度的差异,因此我们可以用后缀子串的长度来表示一个后缀子串,如图:
这里,我们引入一个 suffix 数组,下标表示对应的后缀子串,下标对应的值是该后缀子串在模式串中与其匹配的子串的起始位置下标。当有多个可匹配子串时,采用靠后的那个。如图:
除了需要记录后缀子串能匹配的子串位置外,还需要记录后缀子串能否与模式串前缀子串相匹配,因此引入 prefix Boolean 类型数组数组,下标为对应后缀子串,对应的值为 true 时,表示这个子串与模式串前缀子串匹配。如图:
知道了原理后,我们再来看看如何用程序来计算这两个数组的值,代码非常巧妙。
计算方法:
取模式串
[0,i]部分与模式串求最长公共后缀子串,若公共子串是模式串的前缀子串,则 prefix 数组记录为 true。
// str 表示模式串,m 表示长度,suffix,prefix 数组事先申请好了 private void generateGS(char[] str, 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) { // str[0, i] int j = i; int k = 0; // 公共后缀子串长度 while (j >= 0 && str[j] == str[m-1-k]) { // 与 str[0, m-1] 求公共后缀子串 --j; ++k; suffix[k] = j+1; //j+1 表示公共后缀子串在 str[0, i] 中的起始下标 } if (j == -1) prefix[k] = true; // 如果公共后缀子串也是模式串的前缀子串 } }
现在我们已经得到了 suffix 和 prefix 数组,接下来,考虑一下在好后缀规则中是如何使用这两个数组得到最终的滑动位数呢。
假设当前好后缀长度为
k,那么查看
suffix[k]的值,若不等于
-1,则表示模式串中存在与好后缀匹配的其他子串,并且这个子串的第一个字符位置就是
suffix[k]的值,因此可以得到滑动位数是
j-suffix[k]+1(
j表示坏字符在模式串中的位置)。如图:
若
suffix[k]=-1时,即模式串不存在与好后缀匹配的其他子串,这个时候我们就应该查看当前的好后缀的后缀子串是否存在与模式串前缀子串匹配的情况,假设当前好后缀的后缀子串第一个字符在模式串中的位置是
r,则判断
prefix[m-r]的值(m 为模式串长度),若为
true,则滑动
r位,如图:
最后,若上述两种情况均无匹配,则滑动
m位,如图:
到此,我们已经知道好后缀规则的实现原理,现在可以尝试用具体的代码来实现这个好后缀规则,并且结合前面的坏字符规则,就可以完整实现 BM 算法了。
private static final int SIZE = 256; public int bm(char [] mainStr, int n, char [] str, int m){ int [] index = new int[SIZE]; generateIndex(str, m, index);//记录模式串每个字符最后一次出现的位置 int i = 0;//表示模式串与主串匹配时模式串第一位对应主串的下标 while(i <= n - m){ int j = m - 1; for( ; j >= 0; j--){//从后往前匹配 if(mainStr[i+j] != str[j]) break;//遇到坏字符串 } if(j < 0){ return i;//匹配成功,返回主串中匹配成功的第一个字符的位置 } int ascii = (int)mainStr[i+j];//坏字符串ASCII值 int x = j - index[ascii]; //下面为好后缀规则,上面为坏字符规则 int y = 0; if (j < m-1) {//如果存在好后缀 y = moveByGS(j, m, suffix, prefix); } i = i + Math.max(x, y);//确定最终滑动位数 } return -1; } // j 表示坏字符对应的模式串中的字符下标 ; m 表示模式串长度 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; } //利用散列表存储模式串所有字符位置 private void generateIndex(char [] str, int m, int index[]){ for(int i=0; i<SIZE; i++){ index[i] = -1;//初始化散列表 } for(int i=0; i<m; i++){ int ascii = (int)str[i]; index[ascii] = i; } } //计算模式串的suffix和prefix数组 private void generateGS(char[] str, 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) { // str[0, i] int j = i; int k = 0; // 公共后缀子串长度 while (j >= 0 && str[j] == str[m-1-k]) { // 与 str[0, m-1] 求公共后缀子串 --j; ++k; suffix[k] = j+1; //j+1 表示公共后缀子串在 str[0, i] 中的起始下标 } if (j == -1) prefix[k] = true; // 如果公共后缀子串也是模式串的前缀子串 } }
- 著名字符串匹配算法:KMP算法原理分析和代码实现
- 多模字符串匹配算法原理及Java实现代码
- 多模式串匹配算法:AC 自动机原理、复杂度分析及代码实现
- 第四篇:决策树分类算法原理分析与代码实现
- 第五篇:朴素贝叶斯分类算法原理分析与代码实现
- 第五篇:朴素贝叶斯分类算法原理分析与代码实现
- 朴素贝叶斯分类算法原理分析与代码实现
- 机器学习系列文章:Apriori关联规则分析算法原理分析与代码实现
- 单源最短路 Dijkstra 算法原理详解、代码实现和复杂度分析
- 每天写一点代码----字符串匹配算法 2 (BM算法)
- 传统字符串匹配算法--Brute Force算法的C代码实现
- Logistic回归分类算法原理分析与代码实现
- Apriori 关联分析算法原理分析与代码实现
- 第十四篇:Apriori 关联分析算法原理分析与代码实现
- 多模字符串匹配算法之AC自动机—原理与实现
- 第七篇:Logistic回归分类算法原理分析与代码实现
- 各种算法的C#实现系列1 - 合并排序的原理及代码分析
- 决策树分类算法原理分析与代码实现
- KNN分类算法原理分析及代码实现
- Python实现字符串匹配算法代码示例