字符串匹配算法(二):BM(BoyerMoore)算法、坏字符规则,好后缀规则


BM算法

BM算法的全程叫做Boyer-Moore,是工程上最常用且最高效的字符串匹配算法,有实验统计,它的性能是著名的KMP 算法的 3 到 4 倍。那么它是如何将性能提升的呢?

在上一篇博客中我介绍了BF算法和RK算法,其中也提到过如果想要优化字符串匹配的效率,就必须要减少不必要的比较,例如RK算法就是通过预匹配哈希值来完成了这一功能,但是我们也提到了,由于哈希冲突等原因,RK在最坏的情况下就会退化成BF算法。

基于上述问题的缺陷,BM、KMP(下一篇博客会写)等算法采用了大量滑动的机制来解决这一问题
在这里插入图片描述
在RK和BF算法中,在字符串不匹配的时候,我们通常会将模式串滑动到主串的下一个位置继续进行匹配,这种方法存在一定的缺陷,就是即使我们滑动到的位置不可能完成匹配,我们还是会一个一个去尝试进行配对,这也就是它们效率低下的原因。

就例如上图,我们可以发现a只存在于主串的第一个位置,第四个位置,第六个位置,而其他的位置下模式串是不可能匹配成功的,所以我们滑动的时候就应该直接滑动到上述的位置,如下图
在这里插入图片描述
BM算法的核心就是找到这种大量滑动的规律,减少无意义的匹配。而它正是通过坏字符规则好后缀规则来实现。


坏字符规则

因为我们的坏字符和好后缀规则都需要保证偏移量最大,所以其并非像传统的字符串比较一样从前往后,而是从后往前比较,并且我们将第一个遇见的不匹配的字符称为坏字符

当检测到坏字符后,我们就没有必要再一个一个的进行判断了,因为只有模式串与坏字符T对齐的位置也是字符T的情况下,两者才有匹配的可能。并且为了保证滑动的范围最大,我们对字符T的选择是在模式串中最后一次出现的那个

坏字符规则主要有以下三种情况,下面一一对其进行分析

  1. 模式串中存在与坏字符相同的字符
  2. 模式串中不存在与坏字符相同的字符
  3. 模式串中存在的其他坏字符为第一个字符

情况一:模式串中存在与坏字符相同的字符
在这里插入图片描述
此时的处理方法就是将坏字符与匹配字符对其,接着进行判断
在这里插入图片描述
此时匹配成功

情况二:模式串中不存在与坏字符相同的字符
在这里插入图片描述
此时模式串中不存在可以与坏字符匹配的字符,这也就代表着在坏字符这个位置之前,不可能匹配成功,所以我们直接滑动到坏字符的下一个位置
在这里插入图片描述
此时,匹配成功。

情况三:倒退或者不移动
例如以下情景在这里插入图片描述
后面的全部匹配,不匹配的只有b,而坏字符又在最后面出现过,此时就会倒退。
所以我们还需要加上判断,如果滑动值小于等于0时,就直接向后滑动一步。

为了保存每个字符的最后一次出现的下标,我们使用一个数组来模拟哈希,采用ascii码来进行直接定址
在这里插入图片描述

下面是基于坏字符规则实现的BM算法

//构建坏字符规则的下标数组
void generateBC(const string& pattern, int* indexArr, int len)
{
    //初始化
    for(int i = 0; i < len; i++)
    {
        indexArr[i] = -1;
    }

    //记录模式串中每个下标最后出现的位置
    for(int i = 0; i < pattern.size(); i++)
    {
        indexArr[pattern[i]] = i; 
    }
}

int boyerMoore(const string& str, const string& pattern)
{
    //不满足条件则直接返回false
    if(str.empty() || pattern.empty() || str.size() < pattern.size())
    {
        return -1;
    }
    
    int len1 = str.size(), len2 = pattern.size();
    int indexArr[128] = {0};    //坏字符规则记录数组,记录了每一个字符最后一次出现的下标
    generateBC(pattern, indexArr, 128);
    
    int i = 0;
    while(len1 - i >= len2)
    {
        int j;
        //模式串从后往前匹配
        for(j = len2 - 1; j >= 0; j--)
        {
            //如果当前字符不匹配,则说明该位置是坏字符
            if(str[i + j] != pattern[j])
            {
                break;
            }
        }
        //如果全部匹配,则返回主串起始位置
        if(j < 0)
        {
            return i;
        }
        /*
            如果该字符没出现过,则直接将模式串滑动到坏字符的下一个位置
            如果出现过,则将模式串中对应字符滑动到坏字符处
        */
        int badMove = (j - indexArr[str[i + j]]);
        badMove = (badMove == 0) ? 1 : badMove;   //防止倒退
        i += badMove;
    }
    return -1;
}

从上面也可以看出,在最后一种情况下BM算法的效率就又会退化到BF算法的级别,所以为了防止这种问题,BM还有一种好前缀规则


好后缀规则

在我们进行匹配的时候,我们将第一次碰到的不匹配的字符称为坏字符,而将碰到坏字符之前所匹配到的字符串称为好后缀
在这里插入图片描述
与坏字符规则一样,如果我们想要使得字符串匹配,只有模式串中存在相同子串,并与主串中好后缀对齐的情况下,两者才有匹配的可能。所以直接将对应子串滑动到好后缀的位置,如下图
在这里插入图片描述
如果不存在这个子串,那我们能否按照坏字符规则,则直接跳过好后缀后呢?
在这里插入图片描述
答案是否定的,如果我们因为过度滑动导致我们跳过了本身可匹配的一些字符串,如下图
在这里插入图片描述
这是为什么呢?虽然我们的模式串中并不存在能够与好后缀匹配的子串,但是却存在能够与好后缀部分重合的子串,而我们的滑动就导致了跳过了这些子串
在这里插入图片描述
为了防止上述情况,我们此时就会寻找能够与部分好后缀子串匹配的前缀,并以它为滑动的标准,如下图
在这里插入图片描述
为了方便计算,我们需要保存模式串中所有前缀和后缀的匹配情况以及子串的位置,所以需要引入两个数组,一个是整型数组suffix,其用于标记能够与好后缀匹配的子串的下标。另一个是布尔数组prefix,其用于标记前缀[0, i - 1]是否能够与好后缀进行匹配
在这里插入图片描述

//构建好后缀规则的前缀和后缀数组
void generateGS(const string& pattern, vector<int>& suffix, vector<bool>& prefix)
{
    int len = suffix.size();

    //匹配区间[0 ~ len - 1],len时即为整个模式串,不可能存在前缀
    for(int i = 0; i < len - 1; i++)
    {
        int j = i;
        int size = 0;	//匹配子串的下标

        while(j >= 0 && pattern[j] == pattern[len - 1 - size])
        {
            //继续匹配下一个位置
            j--;    
            size++; 

            suffix[size] = j + 1;   //记录匹配后缀的子串的位置
        }
        //如果子串一直匹配到开头,则说明该子串为前缀,此时前缀与后缀匹配
        if(j == -1)
        {
            prefix[size] = true;
        }
    }
}

//计算出好后缀规则的偏移量
int moveByGS(int index, const vector<int>& suffix, const vector<bool>& prefix)
{
    int len = suffix.size();    //模式串长度
    int size = len - 1 - index;   //后缀长度
    //如果存在与后缀匹配的子串,则直接返回它们的偏移量
    if(suffix[size] != -1)
    {
        return index - suffix[size] + 1;
    }

    //如果没有匹配的后缀,那么判断后缀中是否有部分与前缀匹配
    for(int i = index + 2; i <= len - 1; i++)
    {
        if(prefix[len - i] == true)
        {
            return i;
        }
    }
    //如果也不存在,则说明没有任何匹配,直接偏移整个模式串的长度
    return len;
}

完整代码

好前缀和坏字符都实现了,因为我们希望的是尽量减少不必要的匹配,所以我们选取两者中较大的那一个作为偏移量

将上面的好前缀规则加入前面写的坏字符规则的框架中,就是完整的BM算法,代码如下

//构建坏字符规则的下标数组
void generateBC(const string& pattern, vector<int>& indexArr)
{
    //记录模式串中每个下标最后出现的位置
    for(int i = 0; i < pattern.size(); i++)
    {
        indexArr[pattern[i]] = i; 
    }
}

//构建好后缀规则的前缀和后缀数组
void generateGS(const string& pattern, vector<int>& suffix, vector<bool>& prefix)
{
    int len = suffix.size();

    //匹配区间[0 ~ len - 1],len时即为整个模式串,不可能存在前缀
    for(int i = 0; i < len - 1; i++)
    {
        int j = i;
        int size = 0;

        while(j >= 0 && pattern[j] == pattern[len - 1 - size])
        {
            //继续匹配下一个位置
            j--;    
            size++; 

            suffix[size] = j + 1;   //记录匹配后缀的子串的位置
        }
        //如果子串一直匹配到开头,则说明该子串为前缀,此时前缀与后缀匹配
        if(j == -1)
        {
            prefix[size] = true;
        }
    }
}

int moveByGS(int index, const vector<int>& suffix, const vector<bool>& prefix)
{
    int len = suffix.size();    //模式串长度
    int size = len - 1 - index;   //后缀长度
    //如果存在与后缀匹配的子串,则直接返回它们的偏移量
    if(suffix[size] != -1)
    {
        return index - suffix[size] + 1;
    }

    //如果没有匹配的后缀,那么判断后缀中是否有部分与前缀匹配
    for(int i = index + 2; i <= len - 1; i++)
    {
        if(prefix[len - i] == true)
        {
            return i;
        }
    }
    //如果也不存在,则说明没有任何匹配,直接偏移整个模式串的长度
    return len;
}

int boyerMoore(const string& str, const string& pattern)
{
    //不满足条件则直接返回false
    if(str.empty() || pattern.empty() || str.size() < pattern.size())
    {
        return -1;
    }
    
    int len1 = str.size(), len2 = pattern.size();
    vector<int> indexArr(128, -1);    //标记匹配坏字符的字符下标
    vector<int> suffix(len2, -1);       //标记匹配后缀的子串下标
    vector<bool> prefix(len2, false);        //标记是否匹配前缀
    generateBC(pattern, indexArr);
    generateGS(pattern, suffix, prefix);

    int i = 0;
    while(len1 - i >= len2)
    {
        int j;
        //模式串从后往前匹配
        for(j = len2 - 1; j >= 0; j--)
        {
            //如果当前字符不匹配
            if(str[i + j] != pattern[j])
            {
                break;
            }
        }
        //如果全部匹配,则返回主串起始位置
        if(j < 0)
        {
            return i;
        }
        int badMove = (j - indexArr[str[i + j]]); //坏字符规则偏移量
        badMove = (badMove == 0) ? 1 : badMove;   //防止倒退
        int goodMove = 0;   //好后缀规则偏移量

        //如果一个都不匹配,则不存在后缀
        if(j < len2 - 1)
        {
            goodMove = moveByGS(j, suffix, prefix);   //计算出好后缀的偏移量
        }

        i += max(goodMove, badMove);  //加上最大的那个
    }
    return -1;
}
  • 6
    点赞
  • 18
    收藏
  • 打赏
    打赏
  • 0
    评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:编程工作室 设计师:CSDN官方博客 返回首页
评论

打赏作者

凌桓丶

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值