字符串匹配算法

目录

Brute-Force算法

Knuth-Morris-Pratt算法

确定有限状态自动机

部分匹配表

Boyer-Moore算法

Rabin-Karp算法

总结


网络信息中充满大量的字符串,对信息的搜寻至关重要,因此子字符串查找(即字符串匹配)是使用频率非常高的操作:给定一段长度为N的文本和长度为M的模式字符串(N≥M),在文本中找到一个和模式串相匹配的子串。由这个问题可以延伸至统计模式串在文本中出现的次数、找出上下文(和该模式串相符的子字符串周围的文字)等更复杂的问题。

Brute-Force算法

Brute-Force算法属于暴力搜索,它在文本中对可能匹配模式串的任何位置检查匹配是否存在。一个指针i跟踪文本,另一个指针j跟踪模式串。对于每一个i都会启动一次匹配搜寻,若模式匹配则返回i,否则重置j为0并将i移动到下一个位置进行下一次匹配。

int BruteForce(const string &str, const string &pat)
{
    for (string::size_type i = 0; i <= str.size()-pat.size(); ++i)
    {
        string::size_type j;
        for (j = 0; i < pat.size(); ++j)
        {
            if (str.at(i+j) != pat.at(j))
            {
                break;
            }
        }
        if (j == pat.size())
        {
            return i;
        }
    }
    return -1;
}

下面的程序提供了另一种实现,这种实现中指针i相当于上一段代码中的i+j,即指向文本中已经匹配过的字符串的末端,指针j则记录应该回退的位置。如果i不匹配则回退两个指针:将j重新指向模式串的开头,将i指向文本中本次匹配的开始位置的下一个位置。 

这种实现的代码并不比上一段代码优雅,对于第一个字符就不匹配的情况下还多了一次减法运算和赋值操作。但这种指针回退的实现思路对于理解KMP算法具有指导意义。

int BruteForce(const string &str, const string &pat)
{
    string::size_type i, j;
    for (i = 0, j = 0; i < str.size() && j < pat.size(); ++i)
    {
        if (str.at(i) == pat.at(j))
        {
            ++j;
        }
        else
        {
            i -= j; //j记录了当字符串不匹配时应该回退的位置
            j = 0;
        }
    }
    if (j == pat.size())
    {
        return i-pat.size();
    }
    return -1;
}

 BF算法是一个简单而广泛使用的暴力算法,虽然它在最坏情况下的运行时间与MN成正比,但在实际应用场景中,大部分情况它的运行时间一般与M+N成正比。

Knuth-Morris-Pratt算法

 在某些字符串匹配中,文本串中有许多子串与模式串相似但又不相同。如在aaaaaaaaaaaaab中寻找aab,如果用BF算法,每一次不匹配时文本串指针i都要回退到上一次匹配的开始位置的下一位置重新开始,这实际上对i~i+j之间的字符做了多次比较,重复做了许多无用功。实际上文本串指针i可以不回退,只要回退模式串指针j就可以了。

KMP算法的目标就是免去这些无意义的重复工作,它可以让模式串指针j回退尽可能少,因为在一次不匹配时,其前面检测过已经匹配的部分字符是有可能在下一次匹配时使用的。

算法涉及到前缀和后缀的概念:如果存在A=Sb(A、S为非空字符串),则称S为A的前缀;同样,如果存在A=bS(A、S为非空字符串),则称S为A的后缀。因此只要找到已匹配的子串中相等且最长的前缀和后缀,前缀(或后缀)的长度k就是在下一轮匹配中可以跳过无需检验(因为已经匹配)的子串长度,那么模式串指针j只需要回退j-k即可。

确定有限状态自动机

KMP算法寻找匹配字符串的核心过程可以用确定有限状态自动机(Deterministic Finite Automation,DFA),对于每一个状态的转换都有一定的转换条件,在字符串匹配中,已匹配的字符串长度就是状态,而当前状态的转换则由下一个字符来决定。如下图所示,对于每一个状态的所有转换,只有一条是匹配转换(从j到j+1),其他都是非匹配转换。

 KMP算法就实现了这么一个有限状态自动机dfa[][]。对于每一个字符c,在比较了c和pat[j]之后,dfa[c][j]表示的是应该和下一个文本字符比较的模式字符的位置。在查找中,dfa[str[i][j]是在比较了str[i]和pat[j]之后应该和str[i+1]比较的模式字符的位置。在匹配成功时会继续比较下一个字符,因此dfa[pat[j]][j]总是j+1。在不匹配时,不仅可以知道str[i]的字符,也可以知道文本串中的前j-1个字符,它们就是模式中的前j-1个字符。

搞明白了dfa的作用后,下一步就是如何构造dfa的问题。以下图为例:

ababac在第6个字符不匹配时,我们已经知道前5个字符“ababa”的信息。从前后缀的角度考虑,已匹配的字符串的前缀集为{a, ab, aba, abab},后缀集为{a, ba, aba, baba},从而得出前缀集和后缀集的交集中最长的是“aba”,长度为3,因此模式串指针j应该回退到第3个位置(即pat[2]),在下一轮匹配时从第4个位置开始比较。寻找最长相同前后缀最简单的办法就是固定文本串,并向右移动模式串,就像扫描已匹配的子串一样。

那么dfa应该如何处理下一个字符?通过DFA可以知道完全回退之后算法会扫描ababa并到达第4个状态(序号为3),因此可以将dfa[c][3]复制到dfa[c][5](c为字符)并将c所对应的元素的值设为6,因为pat[5]=c。因为在计算DFA的第j个状态时只需要知道DFA是如何处理前j-1个字符的,所以总能从尚不完整的DFA中得到所需的信息。

const int k = 256; //字符范围

void buildDFA(vector<vector<int>> &dfa, const string &pat)
{
    dfa[pat[0]][0] = 1;
    for (int i = 0, j = 1; j < pat.size(); ++j)
    {
        for (int c = 0; c < k; ++c)
        {
            dfa[c][j] = dfa[c][i]; //匹配失败情况下需要将dfa[][i]复制到dfa[][j]
        }
        dfa[pat[j]][j] = j+1; //设置匹配成功情况下的值
        i = dfa[pat[j]][i];
    }
}

int KMP(const string &str, const string &pat)
{
    vector<vector<int>> dfa(k);
    for (auto &vec : dfa)
    {
        vec.resize(pat.size());
    }
    buildDFA(dfa, pat); //构造dfa

    string::size_type i, j;
    for (i = 0, j = 0; i < str.size() && j < pat.size(); ++i)
    {
        j = dfa[str[i]][j];
    }
    if (j == pat.size())
    {
        return i-pat.size();
    }
    return -1;
}

按照上述方法构造的DFA会占用RM空间(R为字母表大小),另一种方法是在构造DFA时为每个状态设置一个匹配转换和一个非匹配转换(而非指向每个可能出现的字符的多个转换),即我们仅仅追踪每个状态对应的prev状态,然后建立一种动态的有限自动机——每当读入一个新的字符以后,如果匹配,则跳到下一个状态,否则回溯(退化)到prev状态(上一个状态),再看是否匹配。根据KMP状态机的结构特性,这样的过程最终会在0状态收敛。对于非零状态,我们知道状态数会递增的条件是当且仅当发生匹配且匹配连续,一旦有不连续情况发生,则必然产生状态退化。

这种动态的DFA需要一个叫部分匹配表的数组的支持。

部分匹配表

部分匹配表(Partial Match Table,PMT)是KMP算法使用动态DFA匹配的核心。PMT的每一个元素值都代表着当前已匹配子串的前缀集和后缀集的交集中最长的元素。以字符串“abababca”为例,其PMT如下图所示:

 例如对子串“aba”来说,其前缀集为{a, ab},后缀集为{a, ba},交集为{a},即前后缀交集中最长的元素长度为1,因此pmt[2]为1。

理解了PMT后,算法步骤也就很清晰了:

(1)寻找前缀后缀最长公共元素长度,构造PMT

(2)根据PMT构造next数组

       next数组考虑的是当前字符之前的字符串前后缀的相似度,所以通过步骤(1)求得各个前缀后缀的公共元素的最大长度后,只要稍作变形即可:将第①步骤中求得的值整体右移一位,然后初值赋为-1,因此next数组可以直接在PMT上构造。

(3)根据next数组进行匹配

void buildNext(vector<int> &next, const string &pat)
{
    next[0] = -1;
    int i = 0, j = -1;
    while (i < pat.size()
  • 5
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值