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表的构建
在实现好后缀规则之前我们同样需要解决一个问题:“如果出现了好后缀,才能确定模式串中是否存在同样的好后缀,如果模式串中没有同样的好后缀我们又该怎么去寻找我们需要的那个‘前缀’”。
想要实现这个功能我们需要构建两个表:suffix
和prefix
,接下来先了解一下这两个表的具体作用:现在假设模式串是 cabcab
,对模式串建立suffix
和prefix
表格如下:
**suffix[i]表示长度为 i 的后缀在模式串中相匹配的另一个子串的位置**
**prefix[i]表示长度为 i 的后缀与模式串前缀是否相等**
后缀子串 | 子串长度 | suffix[i] | prefix[i] |
---|---|---|---|
b | 1 | 2 | false |
ab | 2 | 1 | false |
cab | 3 | 0 | true |
bcab | 4 | -1 | false |
abcab | 5 | -1 | false |
我认为这张表格的实现是好后缀规则中最难也是最不容易想出来的,只要这张表格构建成功了,其它的就容易了。
- 代码实现
/*
@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;
}