实现串匹配的并行算法_字符串匹配 ---- BM 算法原理

本文介绍了Boyer-Moore算法,一种高效的字符串匹配算法,常用于文本编辑器的查找功能。文章讲解了坏字符匹配和好后缀规则,通过预处理和散列表提高匹配效率,降低无效移动。
摘要由CSDN通过智能技术生成

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

前置内容

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

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

3e658e03c66a5f683f7c8aefb4866687.png

所以到这里, 我们就可以说, 更高效的字符串匹配就是更高效的模式串 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;
}

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

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

坏字符匹配

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

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

256de5316ac54d0eb6de7689d403952f.png

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

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

/**
 * 
 * @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 - 1
    let j = -1 //记录坏字符主串中的下标
    let k = -1 // 记录模式串中对应的坏字符下标
    let badChar = '' // 记录坏字符
    for (let i = len; i >= 0; i--) {
        if (substr[i] !== pattern[i]) {
            j = i
            badChar = substr[i]
            break
        }
    }
    if (j > 0) {
        k = bc[badChar.charCodeAt()]
    }
    return {
        patternBadCharIndex: k,
        mainBadCharIndex: j
    }
}

好后缀规则

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

96800d476d53b17f16f8e07105929058.png

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

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

00f8c730bc0f3fbfec67b5a298196604.png

前缀子串也是同理.

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

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

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

57d76491131137ad79e6fcf4042219d0.png

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

第一步: 引入 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.length
    const 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.length
    const 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.length
    const patternLen = pattern.length
    const bc = generatebc(pattern)
    const { suffix, prefix } = generateGS(pattern)
    let step = 1
    // i, start index of main string
    for (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 - patternBadCharIndex
        if (mainBadCharIndex === -1) { // mainBadCharIndex 坏字符出现的位置, 为 -1 时说明没有坏字符,在开始位置就匹配了
            return i
        }
        let stepForGS = -1
        if (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 - 1
    let j = -1 //记录坏字符主串中的下标
    let k = -1 // 记录模式串中对应的坏字符下标
    let badChar = '' // 记录坏字符
    for (let i = len; i >= 0; i--) {
        if (substr[i] !== pattern[i]) {
            j = i
            badChar = substr[i]
            break
        }
    }
    if (j > 0) {
        k = bc[badChar.charCodeAt()]
    }
    return {
        patternBadCharIndex: k,
        mainBadCharIndex: j
    }
}

console.log('查询结果为: ' + Bm('yaoyaozhuona', 'zhuo'))
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值