amazzzzzing

导航

数据结构 - 模式串匹配 - KMP算法

数据结构 - 模式串匹配 - KMP算法

〇、关于

  • KMP算法:由D.E.Knuth,V.R.Pratt,J.H.Morris同时发现。[1]
  • 部分内容参考《数据结构考研复习指导》·王道论坛
  • KMP算法是对双指针暴力搜索算法的改进
  • 文中序号皆为索引值,从0开始(因此结论会有区别)
  • 文中的KMP算法基于部分匹配值表(PM表)

一、前置

示例字串:abcac

  • 前缀
    指除去最后一个字符,字符串的所有头部字串。示例字串的前缀有
  • 后缀
    指除去最后一个字符,字符串的所有尾部字串。示例字串的后缀有
  • 部分匹配值
    指字符串的前缀和后缀的最长一致长度。示例字串的部分匹配值为0。
    在字符串匹配过程中,已匹配字符串(和模式字符串一致的部分)的部分匹配值表示其利用价值。
  • 部分匹配值表

将字符串的所有头部字串求部分匹配值,得到部分匹配值表(Partial Match, PM)。示例字串的PM表为:

部分匹配表
id | 0 | 1 | 2 | 3 | 4

  • | - | - | - | - | -
    S | a | b | c | a | c
    PM | 0 | 0 | 0 | 1 | 0

二、已匹配字符串的特征

示例目标字符串:ababcabcacbab
示例模式字符串:abcac

  • 在进行字符串模式匹配过程中,当出现不匹配时,向前移动目标字符串指针。对于已匹配的字符串(也即一致的字符串),向前移动的距离需要参考最大的一致前后缀长度,例如:
ababcabcacbab
abcac

在匹配至第三个字符c时,出现不匹配。此时已经匹配的字符串为ab,其最大的一致前后缀长度为0,表示在字符串长度内,不会出现任何匹配,则下一个要匹配的位置就需要跳过整个已匹配字符串。为

ababcabcacbab
  abcac

此时在匹配到第五个字符c时,出现不匹配。此时已经匹配的字符串为abca,其最大一致前后缀长度为1,表示已匹配的四个字符中,最后一个字符需要重新比较,则下一个需要匹配的位置就是最后一个字符。为

ababcabcacbab
     abcac

三、已知PM表的匹配算法

由上述分析可知,当匹配失败时,下一个要匹配的位置需要移动的距离和最后一个已匹配字符的PM值和已匹配的字符串长度有关。

匹配失败时,记PM表为\(PM[ ]\),需要往回移动的距离为\(Move\),模式字符串指针为\(j\)(从0开始),则有

\(Move=j-PM[j-1]\)

\(PM\)表右移一位得到表\(next[]\),则为

\(Move=j-next[j]\)

新的指针位置为

\(j=j-Move=next[j]\)

也即匹配失败时,直接将模式字符串指针移动至 \(next[j]\) (目标字符串指针不需要移动),然后重新开始比较。

四、求模式字符串的next表

由上述推导可知,next表可以看作是PM表右移一位得到。因此先推导PM表。

假设 \(PM[j]=k\) ,模式字符串为 \(p_0p_1p_2...p_{n-1}\) ,则有\(p_0...p_{k-1}=p_{j-k+1}...p_{j}\),且 \(k\) 为满足该式的最大值。

现在需要求 \(PM[j+1]\) ,

\(p[k]=p[j+1]\) ,则有 \(p_0...p_k =p_{j-k+1}...p_{j+1}\) , 即 \(PM[j+1]=k+1\) (由 \(PM[j]=k\) 可以推导)。

\(p[k] \not= p[j+1]\) ,则有 \(p_0...p_k \not=p_{j-k+1}...p_{j+1}\) (且更长的串也不相等)。对于 \(p_{j-k+1}...p_{j+1}\),由于 \(PM[j]=k\) ,因此可以写作 \(p_0...p_{k-1}p_{j+1}\)。对 \(p_0...p_{k-1}p_k\)\(p_0...p_{k-1}p_{j+1}\),则其最长一致前后缀串必然包含了 \(p_0...p_{k-1}\) 的最长前后缀一致串(见下例)。

例如:

     k       j
12312x...12312y...
其中,PM[j] = k = 5
求最长重合串,首先考虑前k个字符的前后缀一致段,对于12312而言,就是PM[4]的值,
为2,即有两个字符是已知一致的,如下:
12312y
   12312x
此时需要判断后一个字符是否相同。
如果不相同,则先考虑前2个字符,对于12,即PM[1],值为0,表示前2个字符无
前后缀一致段,则直接比较极限情况,即y和p[0]是否相同。

即获取 \(PM[k-1]\) 的值,若为0,则直接比较 \(p_{k+1}\)\(p_0\),若不为0,则比较 \(p_{k+1}\)\(p_{PM[k-1]}\) 。如果不相同,且 \(PM[k-1]>0\) ,则获取 \(PM[PM[k-1]-1]\) ,循环查找。

最后将 \(PM\) 表转换为 \(next\) 表即可。

五、基于PM表的一个实现

/**
 * KMP算法
 * D.E.Knuth, V.R.Pratt, J.H.Morris
 * 
 * 有修改
 * 

求PM表:

i         0 1 2 3 4 5 6 7 8
对于模式串 1 2 1 1 2 3 1 2 1
PM        0 0 1 1 2 0 1 2 3
next     -1 0 0 1 1 2 0 1 2
流程:
PM[0] = 0
i = 1, i表示下一个处理的子串
j = 0, j表示上一个处理的子串的PM值
p[j]!=p[i],而j==0,取pm[i]=j,即pm[1]=0
i = i+1 = 2
j = 0
p[j]==p[i],取pm[i]=j+1,即pm[2]=1
i = i+1 = 3
j = j+1 = 1
p[j]!=p[i],取j=pm[j-1],即j=pm[0]=0,
p[j]==p[i],取pm[i]=j+1,即pm[3]=1
i = i+1 = 4
j = 1
p[j]==p[i],取pm[i]=j+1,即pm[4]=2
i = i+1 = 5
j = 2
p[j]!=p[i],取j=pm[j-1],即j=pm[1]=0
p[j]!=p[i],而j==0,取pm[i]=j,即pm[5]=0

...

利用next表进行模式匹配
1 2 1 3 ... 目标字符串
1 2 1 1 2 3 ... 模式字符串
i 为目标字符串指针
j 为模式字符串指针
p[3]匹配失败,查next[3]=1,即
取j=1,继续进行匹配。
如果next为-1,则整体向前一步
(-1代表第一个字符就匹配失败)

 */
const char *strstr_kmp(const char *szSrc, const char *szToken) {
    int nLenToken = strlen (szToken);

    std::vector<int> pm(nLenToken); /* pm表 */

    /* 求模式串的部分匹配(PM)表 */
    pm[0] = 0;
    int i = 1; /* [0,i]表示当前计算pm值的头部子串 */ 
    int j = 0; /* j为上一个已处理字符串的PM值 */
    while (i < nLenToken) {
        if (szToken[j] != szToken[i]) {
            if (j == 0) {
                pm[i]=j;
                ++i;
            }
            else {
                j = pm[j-1];
            }
        }
        else {
            pm[i] = j+1;
            ++i;
            ++j; /* j=pm[i] */
        }
    }

    /* PM表转next表 */
    std::vector<int> next(nLenToken); /* next表 */

    next[0] = -1;
    for (int k = 1; k < next.size(); ++k) {
        next[k] = pm[k-1];
    }

    /* 利用next表进行模式匹配 */
    i = 0; /* i表示szSrc指针 */
    j = 0; /* j表示szToken指针 */
    while (szSrc[i] != '\0' && szToken[j] != '\0') {
        if (szSrc[i] == szToken[j]) {
            ++i;
            ++j;
        }
        else {
            j = next[j];
            if (j < 0) {
                ++i;
                j = 0; /* ++j */
            }
        }
    }

    if (j == nLenToken) {
        return szSrc + i - j;
    }
    else {
        return nullptr;
    }
}

六、KMP算法的优化

目标字符串:a a a b a a a a b
模式字符串:a a a a b
PM         0 1 2 3 0
next      -1 0 1 2 3
匹配失败时,模式字符串向右滑动一位,由于前四个字符是相等的,因此重复了。

当模式字符串中出现了 \(p_j=p_{next[j]}\) 的情况时,对于PM表,也即如果出现 \(p_{j}=p_{pm[j-1]}\) 时,会引起重复比较。此时,取上一个PM值即可(算法中是递进式,因此上一个PM值以保证没有这种问题)。

    /* 求模式串的部分匹配(PM)表 */
    pm[0] = 0;
    int i = 1; /* [0,i]表示当前计算pm值的头部子串 */ 
    int j = 0; /* j为上一个已处理字符串的PM值 */
    while (i < nLenToken) {
        if (szToken[j] != szToken[i]) {
            if (j == 0) {
                pm[i]=j;
                ++i;
            }
            else {
                j = pm[j-1];
            }
        }
        else {
            if (szToken[i+1] == szToken[j+1]) {
                if (j == 0) {
                    pm[i] = 0;
                }
                else {
                    pm[i] = pm[j-1];
                }
            }
            else {
                pm[i] = j+1;
            }
            
            ++i;
            ++j; /* j=pm[i] */
        }
    }

末、引用


  1. 摘自《数据结构》·严蔚敏,4.3.2 - 模式匹配的一种改进算法 ↩︎

posted on 2020-10-31 17:36  amazzzzzing  阅读(447)  评论(0)    收藏  举报