字符串匹配算法(一):BF(BruteForce)算法和RK(RabinKarp)算法

文章目录


字符串匹配是计算机科学中最古老、研究最广泛的问题之一。一个字符串是一个定义在有限字母表∑上的字符序列。例如,ATCTAGAGA是字母表∑ = {A,C,G,T}上的一个字符串。字符串匹配问题就是在一个大的字符串T中搜索某个字符串P的所有出现位置。

为了方便后面讲解,在这里先提出几个概念。

  • 主串和模式串主串即字符串的本体,模式串即用于比对的子串,即我们将要在主串中查找模式串。例如我们要在字符串A中查找字符串B,则A为主串,B为模式串。
  • 单模式匹配算法即在一个主串中匹配一个模式串,例如BF、RK、BM、KMP等算法就是单模式
  • 多模式匹配算法即在一个主串中匹配多个字符串,例如之前讲过的Trie树,以及之后会写的AC自动机。

BF

思路

BF算法是Brute Force的缩写,中文名叫做暴力匹配算法,故名思意,其算法简单粗暴,但对应的性能也不高,算法逻辑如下。
在这里插入图片描述
如上图,BF算法的思路就是从主串的每一个位置进行对比,如果当前位置模式串无法匹配,就移动到下一个位置,继续匹配

实现

该算法的实现十分简单,代码如下

#include<string>
#include<iostream>

using namespace std;

int bruteForce(const string& str, const string& pattern)
{
    
    
    //不满足条件则直接返回false
    if(str.empty() || pattern.empty() || str.size() < pattern.size())
    {
    
    
        return -1;
    }

    int i = 0, j = 0;
    int len1 = str.size(), len2 = pattern.size();
    
    while(i < str.size())
    {
    
    
        while(j < pattern.size())
        {
    
    
            //如果当前不匹配
            if(str[i + j] != pattern[j])
            {
    
    
                i++;    //主串移动到下一个位置
                j = 0;  //模式串回到起始位置
                break;
            }
            j++;    //如果模式串当前位置匹配,则继续匹配下一个位置
        }
        //如果模式串全部匹配,则返回匹配的位置
        if(j == pattern.size())
        {
    
    
            return i;
        }
    }
    return -1;
}

可以看出,这种算法存在着大量无意义的匹配,在最坏的情况下,我们需要依次匹配完主串中的所有位置,因此其时间复杂度高达O(N * M) N为主串长度,M为模式串长度。


RK

思路

那么有什么方法可以减少那些不必要的匹配,提高效率呢?这时候就可以巧妙地借助哈希算法来完成这个任务。

如果对于哈希不了解的可以查看我往期的博客
高级数据结构与算法 | 哈希 :哈希冲突、负载因子、哈希函数、哈希表、哈希桶

RK算法的全称是RabinKarp,是Rabin 和 Karp这两位科学家在BF算法的基础上,加上了哈希的思想后是实现的。

BF算法的主要缺陷在于模式串会去尝试匹配主串的任何一个位置,并且其中每次的匹配都会去一个一个字符进行对比,导致效率的下降。

而RK算法则想到了一个好方法,可以先进行一个预匹配,即哈希值的匹配,如果哈希值不同则说明字符串不同,没有比较的意义,而当哈希值相同时,为了避免哈希冲突的情况,再进行字符串的匹配即可。

由于哈希值只是一个整型,其比较起来的代价比起字符串大大的降低了
在这里插入图片描述

不过,哈希值的计算也需要通过遍历字符串来获得,那么效率不是又变低了吗?这时,就可以通过合理的设计哈希函数,来解决这个问题。

在这里,我选择使用最简单的按位相加来进行举例

首先,我们将模式串以及主串中第一个用来匹配的子串的哈希值计算出来,进行比较。

int hashFunc(const string& str)
{
    
    
    int hashCode = 0;
    for(int i = 0; i < str.size(); i++)
    {
    
    
        hashCode += (str[i] - 'a');
    }

    return hashCode;
}

当其不匹配时,我们需要匹配主串的下一个位置,由于字符串发生变化,哈希值也应该发生变化。

考虑到匹配子串的变化起始就是从主串中向后移动一位,也就是删除了最高位,增加了最低位,所以我们可以借助这个性质,进行增量计算即减去首位的哈希值,加上末尾的哈希值,就可以避免每次都要重复计算哈希值的问题。

//获取移动到下一个位置后的字符串哈希值,即减去开头的哈希值,加上末尾的哈希值
int nextHash(const string& str, int hash, int begin, int end)
{
    
    
    hash -= (str[begin] - 'a');
    hash += (str[end] - 'a');

    return hash;
}

上面这种哈希算法的计算十分简单,但是也存在着大量的哈希冲突,所以选择一个合理的哈希算法,对效率的提升有很大的帮助,但是每种哈希算法也存在着一定的缺陷,例如26进制,虽然其几乎避免了哈希冲突,但是在字符串过长时会导致哈希值的溢出,所以要结合使用场景对哈希函数进行选择。
由于这里只是算法的一个介绍,就直接使用这种最简单的哈希函数。

此时,整个RK算法就包含两个部分,第一部分是进行哈希值的计算,时间复杂度为O(N),第二部分就是进行字符串的匹配,因为哈希值的比较的时间复杂度为O(1),而哈希值的更新也只是简单的增量计算,也是O(1),所以整个算法的时间复杂度为O(N),但是在最坏情况下,如存在大量的哈希冲突时,每次都需要将进行字符串的对比,这时的时间复杂度就会退化回O(N * M)

实现

算法实现如下

#include<string>
#include<iostream>

using namespace std;

//字符串哈希函数,这里使用的是最简单的按位相加
int hashFunc(const string& str)
{
    
    
    int hashCode = 0;
    for(int i = 0; i < str.size(); i++)
    {
    
    
        hashCode += (str[i] - 'a');
    }

    return hashCode;
}

//获取移动到下一个位置后的字符串哈希值,即减去开头的哈希值,加上末尾的哈希值
int nextHash(const string& str, int hash, int begin, int end)
{
    
    
    hash -= (str[begin] - 'a');
    hash += (str[end] - 'a');

    return hash;
}

int rabinKarp(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 patternHash = hashFunc(pattern);
    int subHash = hashFunc(str.substr(0, len2));

    for(int i = 0; i < (len1 - len2 + 1); i++)
    {
    
    
        //如果当前的哈希值相同,则遍历比较字符串是否也相同,如果不同则没有必要进行比较
        if(patternHash == subHash && pattern == str.substr(i, len2))
        {
    
    
            return i;   //如果当前匹配,则返回匹配主串的起始位置
        }

        //如果主串中剩余字符还能与模式串进行对比,则更新哈希值
        if(len1 - i > len2)
        {
    
    
            subHash = nextHash(str, subHash, i, i + len2);
        }
    }
    return -1;
}

猜你喜欢

转载自blog.csdn.net/qq_35423154/article/details/109137760