我们把被搜索的字符串称为文本text,待匹配的字符串称为模式串pattern。BM算法的核心思想就是两个,第一是坏字符,第二是好后缀,好后缀就是pattern与text从右往左连续匹配成功的子串,坏字符就是pattern与text从右往左第一个匹配失败的在text中的字符,如下:
对于坏字符和好后缀,无非就是不同的模式串移动规则,通过各自不同的移动规则,确定分别对于坏字符和好后缀模式串需要移动的位数,最终选择移动位数最大的进行移动即可。
一般来说,文本和模式串都是用数组来表示的,在字符串的匹配过程中,当出现不匹配时,需要进行模式串相对于文本的移动,我们简称为模式串的移动。具体移动方法如下:
下面具体讲解坏字符和好后缀的移动规则,假设文本为text[ i .. i+n ],pattern[ j .. j+m ]。
一. 坏字符移动规则
我们假设坏字符出现在text的 i + k 位置上(也就是pattern的 j + k 位置),我们记坏字符为 bad_char = text[ i + k ]。
现在我们在pattern[ j .. j + k - 1 ]中搜索bad_char(注意:在搜索bad_char的过程中,是从右向左进行匹配的,并且将第一次匹配成功时的位置作为结果返回)
可能出现两种情况:
1) 坏字符没有出现在pattern[ j .. j + k - 1 ]中,此时直接将模式串移动到坏字符的下一个字符位置,相当于pattern移动(j + k) - (j - 1) = k + 1位。
2) 坏字符出现在pattern[ j .. j + k - 1 ]中,位置为pattern[ j + s ],那么pattern移动(j + k)-(j + s)= k - s位。
坏字符移动的思考:如果每次出现bad_char都需要进行一次O(m)时间复杂度的搜索,效率比较低,最好是出现了bad_char,O(1)时间就能确定模式串需要移动的位数。下面看看怎么找到这种优化的方法:
我们记bc[ 'a' ] = j + 1,那么模式串需要移动的位数就等于bad_char的位置减去bc[ 'a' ],即 (j + 4)- (bc[ 'a' ])= 3。
我们记bc[ 'b' ] = j + 3,模式串需要移动的位数(j + 4)- (bc[ 'b' ])= 1
当坏字符在模式串中找不到匹配时,统一移动到坏字符的下一个位置,所以我们记bc[ 'd' .. 'z' ] = j - 1。
模式串需要移动的位数为(j + 4)- (bc[ 'd' ])= 5位。现在再来观察这个bc数组我们能发现,bc[ ch ] 的
值就是字符ch在模式串中最右边出现的位置索引,因为还有一个字符c没有在bc数组里面确定值,根据发现,
可以假设bc[ 'c' ] = j + 4。现在有了bc数组,我们再来看一种情况:
这种情况,我们来计算模式串需要移动的位数,(j +3)-(bc[ 'c' ] ) = -1,这是一个小于零的值,意味着要对
模式串做负的移动,肯定不对,我们再观察,就可以发现这种情况同样直接将模式串移动到bad_char的下一个
字符位置就可以了。即移动的位数为(j + 3)- (j - 1) = 4位。
最终,我们可以确定优化坏字符移动的方法,首先针对模式串计算辅助数组bc[ ch ]:
1) 若ch出现在模式串中,则bc[ ch ]的值为ch在模式串中最右边出现的位置索引。
2) 若ch没有出现在模式串中,则bc[ ch ]的值为 j - 1,j为模式串第一个字符索引,从这里可以看出,
在求bc数组时,只需要关心模式串,即与文本无关。
3) 在通过bc数组计算模式串移动的位置时,需要注意,如果移动的位置为负值,则等同于
将模式串移动到坏字符的下一个位置。
二. 好后缀的移动规则
suffix[ i ]:表示从模式串的位置 i 开始向前扫描,看能够与整个模式串的后缀匹配的最大长度,我们把这个长度作为suffix[ i ]的值。下面用图解释一下就清楚了:
好了,有了suffix数组,接下来计算gs数组。gs[ i ]:i表示好后缀前一个字符,即坏字符的位置,gs[ i ]的值表示模式串需要移动的位数。
计算gs数组时分三种情况:
1)模式串中存在子串与好后缀匹配;
2)模式串中不存在子串与好后缀匹配,但是存在最大前缀与好后缀匹配;
3)模式串中即不存在子串与好后缀匹配,也不存在最大前缀与好后缀匹配。
三. 代码
int* CreateBC(char* pattern, int len)
{
int* bc = new int[256];
for(int i = 0; i < 256; ++i)
{
bc[i] = -1;
}
for(int i = 0; i < len; ++i)
{
bc[pattern[i]] = i;
}
for(int i = 0; i < 256; ++i)
{
if(bc[i] != -1)
{
cout << "bc[" << i << "] = " << bc[i] << endl;
}
}
return bc;
}
int* CreateSuffix(char* pattern, int len)
{
int* suffix = new int[len];
suffix[len - 1] = len;
for(int i = len - 2; i >= 0; --i)
{
int j = i;
for(; pattern[j] == pattern[len - 1 - i + j] && j >= 0; --j);
suffix[i] = i - j;
}
for(int i = 0; i < len; ++i)
{
cout << "suffix[" << i << "] = " << suffix[i] << endl;
}
return suffix;
}
int* CreateGS(char* pattern, int len)
{
int* suffix = CreateSuffix(pattern, len);
int* gs = new int[len];
/*
在计算gs数组时,从移动数最大的情况依次到移动数最少的情况赋值,
确保在合理的移动范围内,移动最少的距离,避免失配的情况。
*/
//第三种情况
for(int i = 1; i < len; ++i)
{
gs[i] = len;
}
//第二种情况
for(int i = len - 1; i >= 0; --i) //从右往左扫描,确保模式串移动最少。
{
if(suffix[i] == i + 1) //是一个与好后缀匹配的最大前缀
{
for(int j = 0; j < len - 1 - i; ++j)
{
if(gs[j] == len) //gs[j]初始值为len, 这样确保gs[j]只被修改一次
{
gs[j] = len - 1 - i;
}
}
}
}
//第一种情况
for(int i = 0; i < len - 1; ++i)
{
gs[len - 1 - suffix[i]] = len - 1 - i;
}
return gs;
}
int bm_search(char* text, int text_len, char* pattern, int pattern_len)
{
int* bc = CreateBC(pattern, pattern_len);
int* gs = CreateGS(pattern, pattern_len);
for(int i = 0; i <= text_len - pattern_len; )
{
int j = pattern_len - 1;
for(; j >= 0 && pattern[j] == text[i+j]; --j);
if(j < 0)
{
return i;
}
int bad_char_index = j;
char bad_char = text[i + j];
int bc_move = bad_char_index - bc[bad_char];
if(bc_move < 0)
{
bc_move = 1;
}
int gs_move = gs[bad_char_index];
int move = (bc_move > gs_move ? bc_move : gs_move);
i += move;
}
if(bc != NULL)
{
delete bc;
bc = NULL;
}
if(gs != NULL)
{
delete bc;
gs = NULL;
}
return -1;
}