关于字符串匹配有很多算法, BF, BK, KMP, 但这些都不是这篇文章的重点. 文章的重点是另外一种更高效的算法 Boyer-Moore 算法, 一般文本编辑器中的查找功能都是基于它实现的.

前置内容

什么是字符串匹配? 它只不过是在一堆字符串中查找特定的字符串的行为, 就像下面的例子一样.

现在为了方便起见, 我们把一堆的字符串称为主串 main, 特定的字符串称为模式串 pattern, 查找过程就可以理解为不断移动 pattern 的过程. 我们从主串的第一个字符开始, 逐个匹配, 遇到不匹配的字符, 就将 pattern 移动一位,直到全部匹配上.

所以到这里, 我们就可以说, 更高效的字符串匹配就是更高效的模式串 pattern 移动. 如何进行高效的移动就是算法的意义所在.

暴力匹配

如果我们用一般方法来完成字符串匹配, 我们可以用一个循环来完成. 试着想象, 我们把模式串抽象的想象为一个字符, 同样的主串中的所有子串我也都抽象的想象为一个字符, 这个过程就变为了对一个字符的匹配, 代码就可以写成下面这样:

function search(main, pattern) {if (main.length === 0 || pattern.length === 0 || main.length < pattern.length) {return -1}for (let i = 0; i <= main.length - pattern.length; i++) {let subStr = main.slice(i, i + pattern.length) // 此处可以想象为一个字符if (subStr === pattern) {return i}}return -1;
}

这个方法简单粗暴, 所以有个名副其实的称呼叫做暴力匹配算法.

虽然我们上面把主串的子串, 和模式串都抽象为一个字符, 但实际比较时, 我们还是需要另一个循环来比较每个字符. 于是有人想了个改进的方法, 对主串中的子串和模式串进行哈希, 这样就每次比较哈希值就可以了. 但是这种方法, 治标不治本, 并没有提高移动模式串的效率.

坏字符匹配

现在我们来说另外一种匹配方法, 倒着匹配.

为什么要倒着匹配? 仔细的想一下我们在正方向匹配的时候, 遇到不匹配的字符我们能做什么?我们可以将字符一位一位的移动进行暴力匹配, 也可以拿主串中不匹配的字符在模式串中找到相等的字符, 然后移动该字符到主串中对应的位置, 如下图.

原本我们是正着过来的, 但现在却需要返回去, 那为什么不从一开始我们就倒着进行匹配? 我们从模式串的末尾倒着匹配, 当发现主串中无法匹配的字符时, 我们就把这个字符称为坏字符. 然后我们就可以像上面说的在模式串中找相等的字符, 然后移动. 如果模式串中没有相等的字符, 我们就将整个模式串移动到坏字符的后面.

好了, 现在我们知道了基本原理, 但还有个问题没解决, 我们怎么知道模式串的前面有没有和坏字符相等的字符?这里我们就需要一个技巧性的预处理, 用一个散列表来存储模式串中的字符和下标, 这样就可以快速定位字符的位置, 下面是一个最简单的实现:

/*** * @param {String} pattern* @description 以 ascii 码作为下标, 存储坏字符串在模式串中的位置 */
function generatebc(pattern) {const bc = new Array(265).fill(-1)for (let i = 0; i < pattern.length; i++) {const index = pattern[i].charCodeAt()bc[index] = i}return bc
}/*** * @param {String} substr 主串中的子串* @param {String} pattern 模式串* @param {Array} bc* @description 查找坏字符串是查找主串中不匹配的字符在模式串中的位置* 假如主串中坏字符的位置对应的模式串中的位置是 si, 我们在模式串中找到同样的字符位置在 xi,* * 那么 si - xi, 就是模式串要移动的位置。*/
function findBadChar(substr, pattern, bc) {let len = substr.length - 1let j = -1 //记录坏字符主串中的下标let k = -1 // 记录模式串中对应的坏字符下标let badChar = '' // 记录坏字符for (let i = len; i >= 0; i--) {if (substr[i] !== pattern[i]) {j = ibadChar = substr[i]break}}if (j > 0) {k = bc[badChar.charCodeAt()]}return {patternBadCharIndex: k,mainBadCharIndex: j}
}

好后缀规则

假设我们现在有这样一个主字符串 aaaaaaaaaaaaa, 模式串为 baaa, 我们用坏字符规则来处理, 好了, 计算出来在模式串中的坏字符位置为 -1. 所以, 光凭坏字符是不够的, 我们需要另一种处理方式, 好后缀规则.

像上面的, 我们把已经匹配到的 bcab 字符串, 就称为好后缀, 现在我们就利用它来进行模式串的移动.

在这之前我们要明确两个概念, 后缀子串和前缀子串. 就拿 bcab 来讲, 它的后缀子串如下表:

前缀子串也是同理.

到这里我们就可以去了解好后缀处理的基本规则:

   1. 找出好后缀的所有后缀子串 2. 找出模式串的所有前缀子串3. 找到好后缀中最长的能和模式串的前缀子串匹配的后缀子串注意:好后缀的后缀子串,本身也是模式串的后缀子串,所以我们可以利用这个在模式串中找到另外的对应匹配的字符

我们就可以移动模式串中和好后缀子串相等的字符串到对应的位置.

为了更高效的移动模式串, 我们同样需要一些预处理, 将原本的循环处理成散列表查询.

第一步: 引入 suffix 数组

这个非常简单, 假设我们在好后缀中的某个子串的长度为 k, 它在模式串中的前缀子串中有相等的, 且起始位置为 i, 那么我们就记录 suffix[k] = i, 如果不存在,我们就记录为 suffix[k] = -1.

第二步: 引入 prefix 数组

除了 suffix 数组之外,我们还需要另外一个 Boolean 类型的 prefix 数组,来记录模式串的后缀子串(好后缀的后缀子串)是否能匹配模式串的前缀子串.

/*** @description 处理好后缀* @param pattern 模式串 * suffix: 用子串长度为 k 存储主串的好后缀{u} 对应的子串中 {u*} 对应的起始位置* prefix:用子串长度为 k 存储 模式串中是否存在和好后缀相同的字符串
*/
function generateGS(pattern) {const len = pattern.lengthconst suffix = new Array(len).fill(-1)const prefix = new Array(len).fill(false)for (let i = 0; i < len - 1; i++) {let j = i;let k = 0;while (j >= 0 && pattern[j] === pattern[len - 1 - k]) {j--;k++;suffix[k] = j + 1}if (j === -1) {prefix[k] = true}}return {suffix,prefix}
}

当我们有了有了这两个数组, 我们既可以确定模式串的滑动位数.

}
/*** * @param { Number} badCharStartIndex  坏字符的对应的模式串的下标* @param { Number} patternLength 模式串的长度* @param { Array<-1>} suffix * @param { Array<boolean>} prefix */
function moveByGS(badCharStartIndex, patternLength, suffix, prefix) {let k = patternLength - badCharStartIndex - 1 // 好后缀长度// 完全匹配if (suffix[k] !== -1) {return badCharStartIndex - suffix[k] + 1}// 部分匹配for (let r = badCharStartIndex + 2; r <= patternLength - 1; r++) {if (prefix[patternLength - r]) {return r}}return patternLength
}

至此, 原理部分全部结束, 下面是完整的 JavaScript 代码

function generatebc(pattern) {const bc = new Array(265).fill(-1)for (let i = 0; i < pattern.length; i++) {const index = pattern[i].charCodeAt()bc[index] = i}return bc
}function generateGS(pattern) {const len = pattern.lengthconst suffix = new Array(len).fill(-1)const prefix = new Array(len).fill(false)for (let i = 0; i < len - 1; i++) {let j = i;let k = 0; // 公共后缀子串长度while (j >= 0 && pattern[j] === pattern[len - 1 - k]) {j--;k++;suffix[k] = j + 1}if (j === -1) {prefix[k] = true}}return {suffix,prefix}
}
function moveByGS(badCharStartIndex, patternLength, suffix, prefix) {let k = patternLength - badCharStartIndex - 1 // 好后缀长度// 完全匹配if (suffix[k] !== -1) {return badCharStartIndex - suffix[k] + 1}// 部分匹配for (let r = badCharStartIndex + 2; r <= patternLength - 1; r++) {if (prefix[patternLength - r]) {return r}}return patternLength
}
function Bm(main, pattern) {if (main.length === 0 || pattern.length === 0 || pattern.length > main.length) {return -1}const mainLen = main.lengthconst patternLen = pattern.lengthconst bc = generatebc(pattern)const { suffix, prefix } = generateGS(pattern)let step = 1// i, start index of main stringfor (let i = 0; i <= mainLen - patternLen; i = i + step) {let substr = main.slice(i, i + patternLen)const { patternBadCharIndex, mainBadCharIndex } = findBadChar(substr, pattern, bc)let stepForBC = mainBadCharIndex - patternBadCharIndexif (mainBadCharIndex === -1) { // mainBadCharIndex 坏字符出现的位置, 为 -1 时说明没有坏字符,在开始位置就匹配了return i}let stepForGS = -1if (mainBadCharIndex < patternLen - 1) {stepForGS = moveByGS(patternBadCharIndex, patternLen, suffix, prefix)}step = Math.max(stepForBC, stepForGS)}return -1;
}function findBadChar(substr, pattern, bc) {let len = substr.length - 1let j = -1 //记录坏字符主串中的下标let k = -1 // 记录模式串中对应的坏字符下标let badChar = '' // 记录坏字符for (let i = len; i >= 0; i--) {if (substr[i] !== pattern[i]) {j = ibadChar = substr[i]break}}if (j > 0) {k = bc[badChar.charCodeAt()]}return {patternBadCharIndex: k,mainBadCharIndex: j}
}console.log('查询结果为: ' + Bm('yaoyaozhuona', 'zhuo'))

cell数组变为字符串_字符串匹配 ---- BM 算法原理相关推荐

  1. 分享一下字符串匹配BM算法学习心得。

    字符串匹配BM(Boyer-Moore)算法学习心得 BM算法 是 Boyer-Moore算法 的缩写,是一种基于后缀比较的模式串匹配算法.BM算法在最坏情况下可以做到线性的,平均情况下是亚线性的(即 ...

  2. cell数组变为字符串_cell转字符串

    如果s是一个cell如下: s = 3×1 cell 数组 'j' '北jing' [ NaN] 方法一: s{i}返回字符串    s(2)返回cell类型,s{2}返回string类型 >& ...

  3. 【数据结构与算法】字符串匹配 BM算法

    单模式串匹配 BF 算法和 RK 算法 BM 算法和 KMP 算法 多模式串匹配算法 Trie 树和 AC 自动机 BM算法 BM算法的核心思想是通过将模式串沿着主串大踏步的向后滑动,从而大大减少比较 ...

  4. 字符串匹配-BM算法改进SUNDAY--Boyer-Moore-Horspool-Sunday Aglorithm

    原文:http://blog.csdn.net/zhoubl668/article/details/7321271 BM算法的改进的算法SUNDAY--Boyer-Moore-Horspool-Sun ...

  5. 字符串匹配——BM算法

    1.BM算法是什么? BM算法是一种非常高效的字符串搜索算法.此算法仅对搜索目标字符串(关键字)进行预处理,而非被搜索的字符串.虽然Boyer-Moore算法的执行时间同样线性依赖于被搜索字符串的大小 ...

  6. 【超详细图解】字符串匹配Boyer-Moore算法:文本编辑器中的查找功能是如何实现的?

    关于字符串匹配算法有很多,之前我有讲过一篇 KMP 匹配算法:图解字符串匹配 KMP 算法,不懂 kmp 的建议看下,写的还不错,这个算法虽然很牛逼,但在实际中用的并不是特别多.至于选择哪一种字符串匹 ...

  7. 字符串匹配--Sunday算法 1

    字符串匹配(查找)算法是一类重要的字符串算法(String Algorithm).有两个字符串, 长度为m的haystack(查找串)和长度为n的needle(模式串), 它们构造自同一个有限的字母表 ...

  8. 字符串匹配 KMP算法

    问题描述:字符串匹配即查找待匹配字符串(模式串)p在主串s中的位置.一般处理这种问题往往采用简单粗暴的方法--暴力匹配法.所谓暴力匹配法,就是对主串s的每一个字符与要匹配的字符串p的每个字符进行逐一匹 ...

  9. 字符串匹配——KMP算法

    字符串匹配--KMP算法 ​ 字符串匹配是计算机编程中最常使用到的基础算法之一.字符串匹配相关的算法很多,Knuth-Morris-Pratt(KMP)算法是最常用的之一.最近在学习KMP算法,学习了 ...

最新文章

  1. 【青铜打铁篇】Activiti 工作流从入门到入土?
  2. 矩阵快速幂 学习笔记
  3. HTTP协议实体的基本讲解
  4. Hadoop HIVE
  5. BZOJ2819 Nim(DFS序)
  6. maven识别包下的xml
  7. 数据科学和人工智能技术笔记 十一、线性回归
  8. Linux系统基础(二)
  9. Elasticsearch mysql 增量同步
  10. php loop循环 拿到键名
  11. 甘肃计算机报名准考证打印,2019年9月甘肃计算机等考准考证打印入口已开通
  12. 多应用集中落地,四川区块链产业爆发增长
  13. uplift model的理论与实践
  14. Android 关于display的几个问题
  15. 观察 :人工智能与伦理道德
  16. react——@修饰器——高阶组件的使用——通过装饰器来调用高阶组件——简单修改样式
  17. 2020年黑龙江省大豆种植分布数据
  18. linux 排除多个目录搜索文件,关于linux:使用find命令但排除两个目录中的文件
  19. java中父类与子类有相同属性调谁?取决于左边
  20. NPOI使用说明---设置字体

热门文章

  1. java 二叉排序_java实现二叉排序树
  2. retrofit封装
  3. 基于JAVA+Servlet+JSP+MYSQL的毕业生就业管理系统
  4. springboot--多环境
  5. Sublime Text 2安装汉化破解、插件包安装教程
  6. c#发送简单的post、get请求
  7. Linux 内核简介
  8. 使用库函数API和C代码中嵌入汇编代码两种方式使用同一个系统调用
  9. SBuild 0.2.0 发布,基于 Scala 的构建系统
  10. 关于jqGrid动态改变列的解决方案