Boyer-Moore(BM)字符串匹配算法

BM 算法是一种高效的字符匹配算法,很多文本编辑器中的文本匹配算法用的就是 BM,所以 BM 算法还是值得我们学习的。

1.BM 算法模式串的移动规则

1.1坏字符规则

在这里插入图片描述

坏字符是相对于文本串来说的,文本串中的 H 与模式串中的 D 匹配失败,则说文本串中的 H 是一个坏字符。

  • 坏字符规则:

当文本串中的某个字符跟模式串的某个字符不匹配时,我们称文本串中的这个失配字符为坏字符,此时模式串需要向右移动,移动的位数 = 坏字符在模式串中的位置 - 坏字符在模式串中最右出现的位置。此外,如果"坏字符"不包含在模式串之中,则最右出现位置为-1

第一种情况:

在这里插入图片描述

第二种情况:

在这里插入图片描述

1.2好后后缀规则

先介绍一下什么是好后缀:

在这里插入图片描述

简单点说:如果坏字符的后面存在已经匹配好的文本,我们称已经这个文本为“好后缀”。

  • 好后缀规则:

①:如果坏字符后面出现了好后缀U,并且模式串中除了坏字符后面的好后缀U之外还有好后缀U,则向右移动模式串,使得模式串中倒数第二个好后缀与文本串中的好后缀对齐(如下图)。

在这里插入图片描述

②:如果坏字符后面出现了好后缀U,并且模式串中除了坏字符后面的好后缀U之外没有好后缀U ,则在好后缀中寻找最长子串I,并且这个子串还是这个模式串的前缀,找到后将模式串右移,使模式串前缀I 与文本串中对应的I对齐(语言描述的可能不好,直接看图)。

在这里插入图片描述

③:如果以上两种情况都未出现,则将模式串右移至好后缀的后一位。

在这里插入图片描述

2.代码实现

字符串匹配的问题,就是模式串的移动问题,不同字符匹配算法的不同往往是“该怎么移动模式串,才能使效率最高”,而 BM 算法中的坏字符规则好后缀规则就是一种移动规则。

2.1坏字符规则

  • 构建坏字符表

实现话字符规则的关键问题是:我们怎么才能知道,某个坏字符在模式串中的最右位置是多少?我们需要构建一张表,表中记录了模式串中每个字符的最右位置,这里用 TS 代码来实现一遍。

interface tableType {
    [key:string]: number
}

/*
	@table 坏字符表
	@modelStr 模式串
	坏字符表是一个典型的 key:value 形式,所以用 TS 中的对象存储比较合适
*/
function get_badCharTable(table: tableType, modelStr: string) {
   
    const modellen = modelStr.length; // 模式串长度

    for(let i = 0; i < modellen - 1; i++)
        table[modelStr[i]] = i;
    
}

这段代码的具体思路就是记录每个字符在模式串中最后出现的位置(每个字符在模式串中的最右的位置)。

  • 实现坏字符规则:
/*
	@mainStr 文本串
	@modelStr 模式串
	@start 模式串的起始相对于文本串的位置
	@table 坏字符表
*/
function badCharRule(mainStr: string, modelStr: string, start: number, table: badType): number {
    
    let badCharIndex: number = -1; // 坏字符相对与模式串的位置
    let badChar: string = ""; //坏字符
    
    const modellen = modelStr.length; // 模式串长度
    
    // 通过循环来从右向左匹配模式串和文本串
    for(let i = modellen - 1; i >= 0; i--) {
        
        let mainStrIndex: number = start + i;
        
        if(mainStr[mainStrIndex] !== modelStr[i]) {
            bacCharIndex = i;
            bacChar = mainStr[mainStrIndex];
            break;
        }
    }
    
    if(badCharIndex === -1)
        return 0;
    
    // 如果 table 中有坏字符(table[badChar] !== undefined)
    if(table[badChar] !== undefined)
        return badCharIndex - table[badChar]; 
		// 模式串移动的距离 = 坏字符相对于模式串的位置 - 坏字符在模式串中的最右的位置。
    
    return badCharIndex + 1;  //badCharIndex - (-1) 
}

2.2好后缀规则

2.2.1 suffix,prefix表的构建

在实现好后缀规则之前我们同样需要解决一个问题:“如果出现了好后缀,才能确定模式串中是否存在同样的好后缀,如果模式串中没有同样的好后缀我们又该怎么去寻找我们需要的那个‘前缀’”。

想要实现这个功能我们需要构建两个表:suffixprefix,接下来先了解一下这两个表的具体作用:现在假设模式串是 cabcab,对模式串建立suffixprefix表格如下:

**suffix[i]表示长度为 i 的后缀在模式串中相匹配的另一个子串的位置**

**prefix[i]表示长度为 i 的后缀与模式串前缀是否相等**

后缀子串子串长度suffix[i]prefix[i]
b12false
ab21false
cab30true
bcab4-1false
abcab5-1false

我认为这张表格的实现是好后缀规则中最难也是最不容易想出来的,只要这张表格构建成功了,其它的就容易了。

  • 代码实现
	/*
		@mainStr 文本串
		@modelStr 模式串
		@start 模式串的起始相对于文本串的位置
		@table 坏字符表
	*/
function get_guffix_prefix(modelStr: string, suffix: number[], prefix: boolean[]){

	const modelLen: number = modelStr.length; // 模式串长度
	suffix.fill(-1, 0, modelLen); // 初始化 suffix 数组
	prefix.fill(false, 0, modelLen); // 初始化 prefix 数组

	for (let i: number = 0; i < modelLen - 1; i++) {
		let j: number = i;
		let k: number = 0;
		while (j >= 0 && modelStr[j] === modelStr[modelLen - 1 - k]) {
			j--;
			k++;
			suffix[k] = j + 1;
		}
		if (j === -1)
			prefix[k] = true;
	}
}

2.2.2 好后缀代码实现

/*
	@mainStr 文本串
	@modelStr 模式串
	@start 模式串起始对应与文本串的位置
	@suffix suffix表
	@prefix prefix表
*/
function goodSuffix(mainStr: string, modelStr: string, start: number, suffix: number[], prefix: boolean[]): number {

	let badCharIndex: number = -1; // 坏字符在对应模式串中的位置
	let suffixLen: number = 0; // 好后缀的长度
	let modelLen: number = modelStr.length; // 模式串的长度
	
	// 寻找坏字符的位置
	for (let i: number = modelLen - 1; i >= 0; i--) {
		let mainStrIndex: number = start + i;
		if (mainStr[mainStrIndex] !== modelStr[i]) {
			badCharIndex = i;
			break;
		}
	}

	/*
		badCharIndex === -1 说明模式串可以与文本串对应字符完全匹配,也就不存在
		坏字符,也就不需要使用好后缀来移动了,毕竟已经完成匹配了
		
		badCharIndex === modelLen - 1 说明模式串中最后一个字符对应的是坏字符,
		也就不存在什么好后缀了,没必要在往下走了
	*/
	if (badCharIndex === -1 || badCharIndex === modelLen - 1)
		return 0;

	// 好后缀的长度
	suffixLen = modelLen - 1 - badCharIndex;

	/*
		suffix[suffixLen] !== -1 说明模式串中存在其它相等的好后缀,根据上面的好
		后缀的移动规则则 ① 来移动就可以了。
		
		可能你会问,规则 ① 所说的移动是将模式串中倒数第二个好后缀移动至与文本好后缀对齐
		怎么能确定 suffix[suffixLen] 是倒数第二个好后缀的起始位置呢,这个问题也很好解答
		认真研读一下 suffix和prefix表格的构建代码,并在纸上走一遍我们会发现,这张表格有		  点类似于坏字符表。
		
		这里给大家举个例子:假设模式串为 abgabeabtab,其中好后缀为 ab,在suffix表的构建
		过程中我们发现第一个相同好后缀的起始位置为 0,在找到了第一个 ab 后程序并不是说停
		止了,而是继续向后寻找,找到了第二个 ab(也是倒数第二个 ab)的起始位置是 3,这个
		时候suffix表中的 0 就会被 3 所覆盖了,到此程序才结束。
	*/
	if (suffix[suffixLen] !== -1)
		return badCharIndex - suffix[suffixLen] + 1;

	// 寻找最长前缀
	for (let i: number = badCharIndex + 2; i <= modelLen - 1; i++) {
		if (prefix[modelLen - i])
			return i;
	}

	return modelLen; // 对应规则 ③
}

3.完整代码实现

function main():number {

    let modelStr: string = 'cb';
    let mainStr: string = 'adbcbab';


    let suffix: number[] = new Array(modelStr.length);
    let prefix: boolean[] = new Array(modelStr.length);

    get_guffix_prefix(modelStr, suffix, prefix);

    let table: badType = modelStrIndex(modelStr);
    let start: number = 0; // 模式串在主串中的起始位置
    while (start + modelStr.length <= mainStr.length) {
        let distance = badChar(mainStr, modelStr, start, table);
        let distance2:number = goodSuffix(mainStr, modelStr, start, suffix, prefix);
        let target:number = Math.max(distance2, distance);
        if (target === 0) {
            console.log('匹配到的位置=' + start);
            return start;
        }

        start += target;
        console.log("滑动至=" + start);
    }

    return start;
}

interface badType {
    [key: string]: number
}


// 坏字符表
function modelStrIndex(modelStr: string): badType {
    const table: badType = {};
    const modelLen: number = modelStr.length;
    for (let i = 0; i < modelLen; i++) {
        table[modelStr[i]] = i;
    }
    return table;
}


// 坏字符规则
function badChar(mainStr: string, modelStr: string, start: number, table: badType): number {
    let badCharIndex: number = -1;
    let badChar: string = "";

    for (let i = modelStr.length - 1; i >= 0; i--) {
        let mainStrIndex: number = start + i;
        if (mainStr[mainStrIndex] !== modelStr[i]) {
            badCharIndex = i; // 坏字符在模式串中的位置
            badChar = mainStr[mainStrIndex]; // 坏字符
            break;
        }
    }

    if (badCharIndex == -1)
        return 0;

    if (table[badChar] !== undefined) {
        return badCharIndex - table[badChar];
    }

    return badCharIndex + 1;
}


// 好后缀表构建辅助表
function get_guffix_prefix(modelStr: string, suffix: number[], prefix: boolean[]) {

    const modelLen: number = modelStr.length;
    suffix.fill(-1, 0, modelLen); // 初始化 suffix 数组
    prefix.fill(false, 0, modelLen); // 初始化 prefix 数组

    for (let i: number = 0; i < modelLen - 1; i++) {
        let j: number = i;
        let k: number = 0;
        while (j >= 0 && modelStr[j] === modelStr[modelLen - 1 - k]) {
            j--;
            k++;
            suffix[k] = j + 1;
        }

        if(j === -1)
            prefix[k] = true;
    }
}


// 好后缀规则
function goodSuffix(mainStr: string, modelStr: string, start: number, suffix:number[], prefix:boolean[]): number {

    let badCharIndex: number = -1; // 坏字符在对应模式串中的位置
    let suffixLen: number = 0; // 好后缀的长度
    let modelLen: number = modelStr.length; // 模式串的长度
    for(let i: number = modelLen - 1; i >= 0; i--) {
        let mainStrIndex: number = start + i;
        if(mainStr[mainStrIndex] !== modelStr[i]) {
            badCharIndex = i;
            break;
        }
    }

    if(badCharIndex === -1 || badCharIndex === modelLen - 1)
        return 0;

    suffixLen = modelLen - 1 - badCharIndex;

    if(suffix[suffixLen] !== -1)
        return badCharIndex - suffix[suffixLen] + 1;

    for(let i: number = badCharIndex + 2; i <= modelLen - 1; i++){
        if(prefix[modelLen - i])
            return i;
    }

    return modelLen;
}

来源

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值