kmp算法

2023-12-14 22:51:12

字符串匹配

在字符串a中查找字符串b,b就是模式串。朴素算法是在a中遍历起始位置,一个一个字符比较,不匹配时起始位置往后移动一下。

KMP算法的失配处理的个人理解

讨论基于字符串下标从1开始。
KMP的想法则是:
a [ i ] , b [ j ] a[i],b[j] a[i],b[j]不匹配了。此时已经可以确定的信息是不匹配位置前面的 j ? 1 j-1 j?1个字符都是一样的,即 b [ 1 , j ? 1 ] = a [ i + 1 ? j , i ? 1 ] b[1,j-1]=a[i+1-j,i-1] b[1,j?1]=a[i+1?j,i?1].
b [ 1 , j ? 1 ] b[1,j-1] b[1,j?1]这个子串为 s s s

  • j = 1 j=1 j=1,s是个空串,显然只能将模式串b往后移动1格。
  • j > 1 j>1 j>1,s非空。那 b b b字符串往后移动 c c c格,只要 1 < c < = l e n ( s ) = j ? 1 1<c<=len(s)=j-1 1<c<=len(s)=j?1,就相当于是 s s s的前 k = l e n ( s ) ? c k=len(s)-c k=len(s)?c个字符和后 k k k个字符在比较,一样的情况下,才会接着比较 b [ k + 1.. ] b[k+1..] b[k+1..] a [ i . . ] a[i..] a[i..]。显然移动的步子越小, k k k越大。可能有多个 k ( k < l e n ( s ) ) k(k<len(s)) k(k<len(s))满足以下条件: s的前k个字符组成的子串和后k个字符组成的子串相等
    为了不遗漏可能的匹配,我们应该移动尽可能的少,即找尽可能大的 k k k.
    这些满足前后子串相等的一系列 k k k中,假设最大的是 K K K,对应的移动步数是 C C C,显然移动步数小于 C C C的时候, b b b一定会不匹配,我们直接跳过这些移动步数不会造成遗漏。因此,当失配( a [ i ] ≠ b [ j ] a[i] \neq b[j] a[i]=b[j])时,我们可以直接移动 C C C步。此时 b b b的前 K K K个字符和 a [ i ] a[i] a[i]前面的 K K K字符匹配上了,即 b [ 1.. K ] = a [ i + 1 ? K , i ] b[1..K]=a[i+1-K,i] b[1..K]=a[i+1?K,i],只需要从 b [ K + 1.. ] b[K+1..] b[K+1..] a [ i . . ] a[i..] a[i..]接着尝试匹配即可。 此时的情况和开始的情况是相似的,只是j换成了K。并且 K < j K<j K<j,也就是说规模在缩小。
    next(s) s是一个字符串定义为满足s前k个字符组成的真子串和后k个字符组成的真子串相等的最大k.
    而当模式串b固定的情况下,next(j) j是整数定义为next(b[1..j])
    有了 n e x t ( j ) next(j) next(j)的定义后,我们记 m o v e ( j ) = j ? n e x t ( j ) move(j)=j-next(j) move(j)=j?next(j) m o v e ( j ) move(j) move(j)的含义就是可以解释为至少移动一步的情况下,并且移动之后可以直接从a[i]开始尝试匹配,移动步数要尽可能少的最小移动步数。换句话说,移动步数小于 m o v e ( j ) move(j) move(j)的情况下,还没比较到 a [ i ] a[i] a[i]就失配了。

KMP算法

有了上面的理解,KMP的匹配算法就容易描述了:

  1. 初始化 i = 1 , j = 1 i=1,j=1 i=1,j=1
  2. 尝试匹配:若 i > L a i>L_a i>La?,则结束; 否则尝试比较 a [ i ] , b [ j ] a[i],b[j] a[i],b[j]
  3. 若相等,则各自加1, i = i + 1 , j = j + 1 i=i+1,j=j+1 i=i+1,j=j+1
    1. j > L b j>L_b j>Lb?,说明已经完全匹配了,可直接计算出完全匹配的起始下标(在字符串a中)。为了寻找后续的匹配位置,并且不遗漏答案,此时应移动尽可能少的步数去尝试寻找后续的匹配位置。并且移动之后需要可以直接从a[i]开始尝试匹配,因此移动 m o v e ( j ? 1 ) move(j-1) move(j?1)步(小于这个步数必然失配),所以直接更新 j = n e x t ( j ? 1 ) + 1 j=next(j-1)+1 j=next(j?1)+1. 回到步骤2接着尝试匹配;
    2. j ≤ L b j \leq L_b jLb?,模式串还未完全匹配,回到步骤2;
  4. 若不等,说明失配,得移动。
    1. 如果第一个字符就失配了,即 j = 1 j=1 j=1,显然这种情况失配必须将模式串往后移动一格重新从模式串的第一个开始匹配,因此 i = i + 1 i=i+1 i=i+1; 回到步骤2接着尝试匹配。
    2. 如果 j > 1 j>1 j>1,需要尽可能少的移动,并且移动后可以直接从a[i]开始匹配。显然可以直接移动 m o v e ( j ? 1 ) move(j-1) move(j?1)步,即更新 j = n e x t ( j ? 1 ) + 1 j=next(j-1)+1 j=next(j?1)+1. 回到步骤2接着尝试匹配。

虽然上面讨论看起来分了四种情况,但是,实际上代码可以很简单。
另外,需要注意的是情况3.2和情况4.2使用的都是已匹配串的next值。

刚刚说的是KMP的算法的 n e x t ( j ) next(j) next(j)的含义。那么如何求 n e x t ( j ) next(j) next(j)呢?
朴素的做法自然是根据定义,逐一验证 k = j ? 1 , j ? 2 , ? ? , 0 k=j-1,j-2,\cdots,0 k=j?1,j?2,?,0前后子串是否相等了。单个 n e x t ( j ) next(j) next(j)时间复杂度最坏情况下是 O ( j 2 ) O(j^2) O(j2).所有的则需要 O ( l 3 ) O(l^3) O(l3).

next数组的求取

n e x t ( j ) next(j) next(j) n e x t ( j ? 1 ) next(j-1) next(j?1)的关系

结论1 n e x t ( j ? 1 ) ≥ n e x t ( j ) ? 1 next(j-1) \geq next(j)-1 next(j?1)next(j)?1.
证明:记 n e x t ( j ) = d next(j)=d next(j)=d,则
n e x t ( j ) = d ? b [ 1.. d ] = b [ j + 1 ? d , j ] ? b [ 1.. d ? 1 ] = b [ j + 1 ? d , j ? 1 ] ? n e x t ( j ? 1 ) ≥ d ? 1 = n e x t ( j ) ? 1 next(j)=d \Rightarrow b[1..d]=b[j+1-d,j] \Rightarrow \\ b[1..d-1]=b[j+1-d,j-1] \Rightarrow next(j-1) \geq d-1 = next(j)-1 next(j)=d?b[1..d]=b[j+1?d,j]?b[1..d?1]=b[j+1?d,j?1]?next(j?1)d?1=next(j)?1.
人话解释就是,对于前 j j j个字符构成的子串 b [ 1.. j ] b[1..j] b[1..j]前后相等的一个子串截取方案,只要都前后两个子串都去掉子串的最后一个字符,就是 b [ 1.. j ? 1 ] b[1..j-1] b[1..j?1]的前后相等的方案,因此, n e x t ( j ? 1 ) next(j-1) next(j?1)至少是 n e x t ( j ) ? 1 next(j)-1 next(j)?1.


有了这个结论,我们就可以将 n e x t ( j ) next(j) next(j)的取值范围缩小到 n e x t ( j ? 1 ) + 1 next(j-1)+1 next(j?1)+1以内了,并且由 n e x t ( j ) next(j) next(j)的定义知, b [ 1.. n e x t ( j ? 1 ) ] = b [ j ? n e x t ( c ) , j ? 1 ] b[1..next(j-1)]=b[j-next(c),j-1] b[1..next(j?1)]=b[j?next(c),j?1]
现在考虑如下的子问题:

子问题

子问题: 假设已知 n e x t ( j ) ≤ c + 1 ( c ≥ 0 ) next(j) \leq c+1 (c \geq 0) next(j)c+1(c0),且 b [ 1.. c ] = b [ j ? c , j ? 1 ] b[1..c]=b[j-c,j-1] b[1..c]=b[j?c,j?1]。求 n e x t ( j ) next(j) next(j)的问题记作 f ( c , j ) f(c,j) f(c,j)

  1. c = 0 c=0 c=0,说明 n e x t ( j ) ≤ 1 next(j) \leq 1 next(j)1, b [ 1.. c ] , b [ j ? c , j ? 1 ] b[1..c],b[j-c,j-1] b[1..c],b[j?c,j?1]是两个空串。此时只要比较 b [ j ] b[j] b[j] b [ 1 ] b[1] b[1].
    1. 相等: n e x t ( j ) = 1 next(j)=1 next(j)=1
    2. 不等: n e x t ( j ) = 0 next(j)=0 next(j)=0
  2. c > 0 c>0 c>0,比较 b [ j ] , b [ c + 1 ] b[j],b[c+1] b[j],b[c+1].
    1. 相等,则 b [ 1.. c + 1 ] = b [ j + 1 ? c , j ] b[1..c+1]=b[j+1-c,j] b[1..c+1]=b[j+1?c,j],易知 n e x t ( j ) = c + 1 next(j)=c+1 next(j)=c+1.
    2. 不等,说明 n e x t ( j ) ≠ c + 1 next(j) \neq c+1 next(j)=c+1,因此可以知道 n e x t ( j ) < c + 1 next(j)<c+1 next(j)<c+1.
      因此,问题转化成求一个尽可能大的 d ( d < c ) d(d<c) d(d<c),满足 b [ 1.. d + 1 ] = b [ j ? d , j ] b[1..d+1]=b[j-d,j] b[1..d+1]=b[j?d,j]. b [ 1.. d + 1 ] = b [ j ? d , j ] b[1..d+1]=b[j-d,j] b[1..d+1]=b[j?d,j]可以拆分成两个条件:① b [ 1.. d ] = b [ j ? d , j ? 1 ] b[1..d]=b[j-d,j-1] b[1..d]=b[j?d,j?1] b [ d + 1 ] = b [ j ] b[d+1]=b[j] b[d+1]=b[j].
      对于条件①,因为 b [ 1.. c ] = b [ j ? c , j ? 1 ] b[1..c]=b[j-c,j-1] b[1..c]=b[j?c,j?1],所以取更短( d < c d<c d<c)的区间有 b [ j ? d , j ? 1 ] = b [ c + 1 ? d , c ] b[j-d,j-1]=b[c+1-d,c] b[j?d,j?1]=b[c+1?d,c],即条件①可以等价于③ b [ 1 , d ] = b [ c + 1 ? d , c ] b[1,d]=b[c+1-d,c] b[1,d]=b[c+1?d,c]。根据前面所述的 n e x t ( ? ) next(*) next(?)的定义,显然只要 n e x t ( c ) < d < c next(c) < d < c next(c)<d<c,肯定不满足条件③。
      因此可以进一步缩小 n e x t ( j ) next(j) next(j)范围为: n e x t ( j ) = m a x ( d ) + 1 ≤ n e x t ( c ) + 1 next(j) = max(d)+1 \leq next(c)+1 next(j)=max(d)+1next(c)+1}, 并且显然有 b [ 1.. n e x t ( c ) ] = b [ j ? n e x t ( c ) , j ? 1 ] b[1..next(c)]=b[j-next(c),j-1] b[1..next(c)]=b[j?next(c),j?1]。\
      注意:此时,此时问题已经和原问题相似的子问题了,唯一的不同是 c c c变成了 n e x t ( c ) next(c) next(c)即转化为了问题 f ( n e x t ( c ) , j ) f(next(c),j) f(next(c),j). 由于 n e x t ( c ) < c next(c)<c next(c)<c,所以问题规模一直在缩小。
      由上面的叙述,得到了
      f ( c , j ) = { 0 c = 0 , b [ j ] ≠ b [ c + 1 ] ; ( 1 ) 1 c = 0 , b [ j ] = b [ 1 ] ; ( 2 ) c + 1 c > 0 , b [ j ] = b [ c ] ; ( 3 ) f ( n e x t ( c ) , j ) c > 0 , b [ j ] ≠ b [ c ] ; ( 4 ) = { 0 c = 0 , b [ j ] ≠ b [ c + 1 ] ; ( 1 ) c + 1 b [ j ] = b [ c + 1 ] ; ( 2 , 3 ) f ( n e x t ( c ) , j ) c > 0 , b [ j ] ≠ b [ c + 1 ] ; ( 4 ) f(c,j)= \left\{ \begin{aligned} 0 \quad & c = 0, b[j] \neq b[c+1]; &(1) \\ 1 \quad & c = 0, b[j] = b[1]; &(2)\\ c+1 \quad & c>0,b[j]=b[c]; &(3)\\ f(next(c),j) \quad & c>0,b[j] \neq b[c]; & (4)\\ \end{aligned} \right. \quad =\left\{ \begin{aligned} 0 \quad & c = 0, b[j] \neq b[c+1]; &(1) \\ c+1 \quad & b[j]=b[c+1]; &(2,3)\\ f(next(c),j) \quad & c>0,b[j] \neq b[c+1]; & (4)\\ \end{aligned} \right. \quad f(c,j)=? ? ??01c+1f(next(c),j)?c=0,b[j]=b[c+1];c=0,b[j]=b[1];c>0,b[j]=b[c];c>0,b[j]=b[c];?(1)(2)(3)(4)?=? ? ??0c+1f(next(c),j)?c=0,b[j]=b[c+1];b[j]=b[c+1];c>0,b[j]=b[c+1];?(1)(2,3)(4)?
      其中case 2也符合case 4的表达式,因此可以合并。
c c c的初值的确定

所以就剩下一个问题, n e x t ( j ) = f ( c , j ) next(j)=f(c,j) next(j)=f(c,j)中的最开始的 c c c值该怎么确定呢?由结论1知 n e x t ( j ) ≤ n e x t ( j ? 1 ) next(j) \leq next(j-1) next(j)next(j?1),,恰好符合了 f f f问题的已知(预设),因此 n e x t ( j ) = f ( n e x t ( j ? 1 ) , j ) ( j > 1 ) next(j)=f(next(j-1), j) (j>1) next(j)=f(next(j?1),j)(j>1).
于是只余下边界 n e x t ( 1 ) next(1) next(1)的问题了。根据定义不难知道 n e x t ( 1 ) = 0 next(1)=0 next(1)=0.

小结

n e x t ( j ) = { 0 j = 1 ( 1 ) f ( n e x t ( j ? 1 ) , j ) j > 1 ( 2 ) f ( c , j ) = { 0 c = 0 , b [ j ] ≠ b [ c + 1 ] ; ( 1 ) c + 1 b [ j ] = b [ c + 1 ] ; ( 2 , 3 ) f ( n e x t ( c ) , j ) c > 0 , b [ j ] ≠ b [ c + 1 ] ; ( 4 ) next(j)= \left\{ \begin{aligned} 0 \quad & j = 1 \quad & (1) \\ f(next(j-1), j) \quad & j > 1 \quad & (2) \\ \end{aligned} \right. \quad f(c,j)= \left\{ \begin{aligned} 0 \quad & c = 0, b[j] \neq b[c+1]; \quad &(1) \\ c+1 \quad & b[j]=b[c+1]; \quad & (2,3)\\ f(next(c),j) \quad & c>0,b[j] \neq b[c+1]; \quad &(4)\\ \end{aligned} \right. \quad next(j)={0f(next(j?1),j)?j=1j>1?(1)(2)?f(c,j)=? ? ??0c+1f(next(c),j)?c=0,b[j]=b[c+1];b[j]=b[c+1];c>0,b[j]=b[c+1];?(1)(2,3)(4)?

因为c++数组下标是从0开始的,我们可以对next,f的定义稍作修改:

下标从1开始: n e x t ( j ) next(j) next(j)为使得 b [ 1.. j ] b[1..j] b[1..j]子串中使得 b [ 1.. k ] = b [ j + 1 ? k , j ] ?? ( k < j ) b[1..k]=b[j+1-k,j]\; (k<j) b[1..k]=b[j+1?k,j](k<j)的最大 k k k. k k k是前后相等子串的长度。 定义域是 [ 1 , L b ] [1,L_b] [1,Lb?]
下标从0开始: n e x t ( j ) next(j) next(j)为使得 b [ 0.. j ] b[0..j] b[0..j]子串中使得 b [ 0.. k ? 1 ] = b [ j + 1 ? k , j ] ?? ( k < j ) b[0..k-1]=b[j+1-k,j]\; (k<j) b[0..k?1]=b[j+1?k,j](k<j)的最大 k k k. k k k是前后相等子串的长度。 定义域是 [ 0 , L b ? 1 ] [0,L_b-1] [0,Lb??1]
从1开始计数的 n e x t [ j ] next[j] next[j]就是 b [ 1.. j ] b[1..j] b[1..j]这个字符串的长度,而从0开始的 n e x t [ j ] next[j] next[j]则是 b [ 0.. j ] b[0..j] b[0..j]的长度减1.

下标从1开始: f ( c , j ) f(c,j) f(c,j):已知 n e x t ( j ) ≤ c + 1 ( c ≥ 0 ) next(j) \leq c+1 (c \geq 0) next(j)c+1(c0),且 b [ 1.. c ] = b [ j ? c , j ? 1 ] b[1..c]=b[j-c,j-1] b[1..c]=b[j?c,j?1]。求 n e x t ( j ) next(j) next(j)
下标从0开始: f ( c , j ) f(c,j) f(c,j):已知 n e x t ( j ) ≤ c + 1 ( c ≥ 0 ) next(j) \leq c+1 (c \geq 0) next(j)c+1(c0),且 b [ 0.. c ? 1 ] = b [ j ? c , j ? 1 ] b[0..c-1]=b[j-c,j-1] b[0..c?1]=b[j?c,j?1]。求 n e x t ( j ) next(j) next(j)

那么下标从0开始:
n e x t ( j ) = { 0 j = 0 ( 1 ) f ( n e x t ( j ? 1 ) , j ) j > 0 ( 2 ) f ( c , j ) = { 0 c = 0 , b [ j ] ≠ b [ c ] ; ( 1 ) c + 1 b [ j ] = b [ c ] ; ( 2 , 3 ) f ( n e x t ( c ? 1 ) , j ) c > 0 , b [ j ] ≠ b [ c ] ; ( 4 ) next(j)= \left\{ \begin{aligned} 0 \quad & j = 0 &(1) \\ f(next(j-1), j) \quad & j > 0 &(2) \\ \end{aligned} \right. \quad f(c,j)=\left\{ \begin{aligned} 0 \quad & c = 0, b[j] \neq b[c]; &(1) \\ c+1 \quad & b[j]=b[c]; &(2,3)\\ f(next(c-1),j) \quad & c>0, b[j] \neq b[c]; & (4)\\ \end{aligned} \right. \quad next(j)={0f(next(j?1),j)?j=0j>0?(1)(2)?f(c,j)=? ? ??0c+1f(next(c?1),j)?c=0,b[j]=b[c];b[j]=b[c];c>0,b[j]=b[c];?(1)(2,3)(4)?
匹配的时候,不管下标从0还是1开始计数,不管是失配了需要移动还是已经完全匹配了需要移动找下一个可能的匹配的位置。最少的移动的步数都是 L m a t c h e d _ s t r ? n e x t ( m a t c h e d _ s t r ) L_{matched\_str}-next(matched\_str) Lmatched_str??next(matched_str)。小于这个移动步数则必然会失配,而采取这个移动步数,就会使得移动后模式串的前 n e x t ( m a t c h e d _ s t r ) next(matched\_str) next(matched_str)个字符是匹配的,而 j j j作为下标,只需要根据长度基于0还是1去计算了。

Code

因此,不难得到 n e x t next next数组的求解代码(下标从0开始):

// 下标从0开始计数,nxt[j](0 <= j < n)含义是针对s[0..j]的前后真子串相等的最长长度.
// n是s的长度。
void cal_nxt(char *s, int n) {
    nxt[0] = 0;
    for (int i = 1;i < n; i++) {
        int c = nxt[i-1];
        while (c>0 && s[i] != s[c]) c = nxt[c-1];
        if (s[i] == s[c]) nxt[i] = c+1; else nxt[i] = 0;
    }
}

匹配

// b是模式串,a是文本串。需要在a中找所有b出现的其实位置
int j = 0; // 模式串下标小于j前面的都已经匹配成功了;当下标从0开始计数的时候,j就是已经匹配的长度。
for (int i = 1;i < n; i++) {
    while (a[i] != b[j] && j > 0) j = nxt[j-1]; // 只要失配,并且已经匹配的长度大于0,就可以更新为已经匹配串的next
    if (a[i] != b[j]) continue; // 说明是第一个位置失配了,只能是起始位置往后移动
    ++j; // 匹配成功了,后移
    if (j == m) { // 完全匹配了
        // b在a中出现的起始下标
        cout<<i+2-m<<"\n";
        j = nxt[j-1]; // 已匹配串的nxt
    }
}

文章来源:https://blog.csdn.net/yuyilahanbao/article/details/134932788
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。