模式匹配

Baileys2022年8月4日
  • 数据结构与算法
大约 9 分钟...

模式匹配

子串的定位操作通常称为串的模式匹配,本小节主要介绍简单的模式匹配算法、KMP算法、KMP算法的进一步优化。

简单的模式匹配算法

简单的模式匹配算法,通过暴力搜索。

int Index(string s, string t)
{
    int i = 0, j = 0;
    while(i<s.length() && j < t.length())
    {
        if(s.at(i) == t.at(j))
        {
            i++;
            j++;
        }
        else
        {
            i = i - j +1;
            j = 0;
        }
    }
    if (j >= t.length())
        return i - j;
    return -1;
};

KMP算法

在暴力匹配中,每趟匹配失败都是模式向后移一位再从头比较,这种频繁的重复使模式串不断地自我比较,因此其效率较低。

从分析模式本身的结构来看,若已匹配相等的前缀序列中有某个后缀正好是模式的前缀,那么此时已匹配相等的前缀序列的该后缀及其后面展开区域有可能匹配。

PM表

字符串的前缀、后缀和部分匹配值

  • 前缀:除最后一个字符以外,字符串的所有头部子串。
  • 后缀:除第一个字符以外,字符串的所有尾部子串。
  • 部分匹配值:字符串的前缀和后缀的最长相等前后缀长度。

以'ababa'为例五,'ababa'的部分匹配值为00123

  • 'a'的前缀和后缀为空集,最长相等前后缀长度为0;
  • 'ab'的前缀为{a}\{a\},后缀为{b}\{b\}{a}{b}=\{a\}{\cap}\{b\}=\varnothing,最长相等前后缀长度为0;
  • 'aba'的前缀为{a,ab}\{a,ab\},后缀为{ba,a}\{ba,a\}{a,ab}{ba,a}={a}\{a,ab\}{\cap}\{ba,a\}=\{a\},最长相等前后缀长度为1;
  • 'abab'的前缀为{a,ab,aba}\{a,ab,aba\},后缀为{bab,ab,b}\{bab,ab,b\}{a,ab,aba}{bab,ab,b}={ab}\{a,ab,aba\}{\cap}\{bab,ab,b\}=\{ab\},最长相等前后缀长度为2;
  • 'ababa'的前缀为{a,ab,aba,abab}\{a,ab,aba,abab\},后缀为{baba,aba,ba,a}\{baba,aba,ba,a\}{a,ab,aba,abab}{baba,aba,ba,a}={a,aba}\{a,ab,aba,abab\}{\cap}\{baba,aba,ba,a\}=\{a,aba\},最长相等前后缀长度为3;

'ababa'对应的PM(Partial Match)(部分匹配值)表格如下:

编号12345
Sabcac
PM00010

使用PM表进行字符串匹配:

  • 第一趟匹配过程
主串ababcabcacbab
子串abc

由于'c'和'a'不匹配,前面的2个字符'ab'是匹配的,通过查表可知,最后一个匹配字符'b'对应的部分匹配值为0,因此按照下述公式,计算子串需要向后移动的位数:

移动位数=已匹配的字符数-对应的部分匹配值\text{移动位数=已匹配的字符数-对应的部分匹配值}

因此20=02-0=0,故将子串向右移动22位。

  • 第二趟匹配过程
主串ababcabcacbab
子串abcac

'c'和'b'不匹配,查看最后一个字符'a'的部分匹配值为11,因此41=34-1=3,将子串右移33位。

  • 第三趟匹配过程
主串ababcabcacbab
子串abcac

全部匹配成功,由于匹配过程中,主串没有回退,因此时间复杂度为O(m+n)O(m+n).

KMP算法原理

如下图,当'c'与'b'不匹配时,此时匹配的前缀为'abca',此时'abca'的最长公共元素为'a',因此可以将子串直接移动移动位数=已匹配的字符数-对应的部分匹配值\text{移动位数=已匹配的字符数-对应的部分匹配值}位,即33位。

主串ababcabcacbab
子串abcac
子串abcac
子串abcac
子串abcac

对公式移动位数=已匹配的字符数-对应的部分匹配值\text{移动位数=已匹配的字符数-对应的部分匹配值}解释:

部分匹配值为最长公共匹配前后缀,因此用已经匹配的字符数-对应的部分匹配值,可以使最长的前缀挪到相应的后缀上。

对算法的改进
在使用部分匹配,每当失败时,就要去搜索他前面一个元素的匹配值,使用起来不方便,因此将PM表向右移动一位,此时,只需找匹配失败元素对应的值即可。

编号12345
Sabcac
next-10001
  • 第一个元素右移缺失后使用1-1来填充,因为第一个元素就匹配失败的话,只需将子串向后移动一位,不需要计算移动位数。

  • 最后一个元素在右移过程中溢出,由于最后一个元素没有后继元素需要使用他的部分匹配值,因此可以舍去。

此时推导出的子串移动位数(Move)公式为:

Move=(j1)next[j]\text{Move}=(j-1)-\text{next[j]}

相当于将子串的指针jj回退到

j=jMove=j((j1)next[j])=next[j]+1j = j-\text{Move}=j-((j-1)-\text{next}[j])=\text{next}[j]+1

next数组

next\textbf{next}的含义: 在子串的第jj个字符与主串发生失配时,跳到子串 next[j]\text{next}[j]位置重新与主串当前位置进行比较。

next\textbf{next}公式如下:

next[j]={0, j=1max{k1<k<j,p1pk1=pjk+1pk1,当此集合不为空时}1,其他情况 \textbf{next}[j]=\left\{ \begin{array}{l} 0,\ j=1\\ max\{k|1<k<j,\text{且}p_{1}{\cdots}p_{k-1}=p_{j-k+1}{\cdots}p_{k-1}\text{,当此集合不为空时}\}\\ 1,\text{其他情况} \end{array} \right.

使用计算机对next\textbf{next}数组求解:

    1. 由公式有next[1]=0\textbf{next}[1]=0.
    1. next[j]=k\textbf{next}[j]=k,则:
  • 2.1 若pk=pj\text{p}_k=\text{p}_j,则表明在模式串p1...pk1pk=pjk+1...pj1pj\text{p}_1...\text{p}_{k-1}\text{p}_k=\text{p}_{j-k+1}...\text{p}_{j-1}\text{p}_j中不可能出现k>kk^{'}>k满足上述条件,此时next[j+1]=next[j]+1\textbf{next}[j+1]=\textbf{next}[j]+1.

  • 2.2 若pkpj\text{p}_k{\neq}\text{p}_j,则表明在模式串中p1...pk1pkpjk+1...pj1pj\text{p}_1...\text{p}_{k-1}\text{p}_k{\neq}\text{p}_{j-k+1}...\text{p}_{j-1}\text{p}_j,此时将kk滑动到next[k]\textbf{next}[k],即令k=next[k]k=\textbf{next}[k],若pkpj\text{p}_k{\neq}\text{p}_j,则以此类推,直到找到更小的kk^{'}使k=next[next...[k]](1<k<k<j>>)k^{'}=\textbf{next}[\textbf{next...[k]}](1<k^{'}<k<j>>),满足条件p1...pk1pk=pjk+1...pj1pj\text{p}_1...\text{p}_{k-1}\text{p}_{k^{'}}=\text{p}_{j-k^{'}+1}...\text{p}_{j-1}\text{p}_j,则next[j+1]=k+1\textbf{next}[j+1]=k^{'}+1.

    1. 当不存2.中的kk^{'}满足上述条件时,即不存在长度更短的相等前缀后缀时,令next[j+1]=1\textbf{next}[j+1]=1

KMP算法流程举例

设S='aabaabaabaac',P='aabaac',求P的next\textbf{next}及KMP匹配过程。

使用手工法求next\textbf{next}数组,步骤如下:

  1. 求PM表
j123456
Paabaac
PM010120
  1. 将PM表向右移动一位
j123456
Paabaac
PM Moved-101012
  1. 将PM Moved所有元素加1
j123456
Paabaac
next012123

KMP匹配的过程如下:

匹配过程中进行的单个字符之间的比较次数为6+4+4

  1. i=6,j=6时匹配失败,将P位于next[j]\textbf{next}[j]的数组移到j处。
j123456789101112
Saabaabaabaac
Paabaac
next012123
  1. i=9,j=6时匹配失败,将P位于next[j]\textbf{next}[j]的数组移到j处。
j123456789101112
Saabaabaabaac
Paabaac
next012123
  1. 匹配成功。
j123456789101112
Saabaabaabaac
Paabaac
next012123

KMP的代码实现

采用先求解移位后的PM表,再对所有元素+1的方式实现。

vector<int> get_next(string P) {
    vector<int> next(P.length(), 0);
    next[0] = -1;
    int j = 0, k = -1;
    while (j < P.length()) {
        if (k == -1 || P.at(j) == P.at(k)) {
            k++;
            j++;
            next[j] = k;
        } else
            k = next[k];
    }
    for (int i = 0; i < P.length(); i++)
        next.at(i)++;
    return next;
}

KMP算法的改进-nextval数组

KMP缺陷

前面KMP定义的next\textbf{next}数组在某些情况下存在缺陷,如下表所示。

主串aaabaaaab
模式aaaab
j12345
next01234
nextval00004

使用KMP匹配的过程如下:

  1. 第一次匹配
主串aaabaaaab
模式aaaa
j1234
next0123
  1. 第二次匹配
主串aaabaaaab
模式aaa
j123
next012
  1. 第三次匹配
主串aaabaaaab
模式aa
j12
next01
  1. 第四次匹配
主串aaabaaaab
模式a
j1
next0
  1. 第五次匹配
主串aaabaaaab
模式aaaab
j12345
next01234

可以发现,该算法效率低下的主要的原因在于 Pnext[j]=Pj\text{P}_{\textbf{next}[j]} = \text{P}_{j}, 上述例子中Pnext[4]=3=P4=a\text{P}_{\textbf{next}[4]=3}=P_{4}=aPnext[3]=2=P3=a\text{P}_{\textbf{next}[3]=2}=P_{3}=aPnext[2]=1=P2=a\text{P}_{\textbf{next}[2]=1}=P_{2}=a, 因此后三次使用相同的字符和S4\text{S}_{4}匹配,因此无意义,一定失配。

KMP改进

出现上述问题后,遇到Pnext[j]=Pj\text{P}_{\textbf{next}[j]} = \text{P}_{j}的处理方法:
若出现上述情况,则将next[j]\textbf{next}[\textbf{j}]修改为next[next[j]]\textbf{next[\textbf{next}[\text{j}]]},直至两者不再相等。

评论
Powered by Waline v2.6.1