KMP模式匹配算法分析

                <<KMP模式匹配算法分析>>
            Tags: alg,linux,devel

1. 朴素模式匹配算法(BF)

为了更好地理解某一事物, 最好的办法就是支了解它的发展史. 所以, 在介绍KMP之前,
先介绍一下最初的模式匹配算法(BF), 也称简单模式匹配算法.

BF算法思想很简单, 用模式串(P)的字符依次与目标串(T)的字符做比较,
    T       T0  T1  T2  …  Tm-1  …  Tn-1
    P       P0  P1  P2  …  Pm-1

    如果, T0 = P0, T1 = P1, T2 = P2, …, Tm-1 = Pm-1, 则匹配成功, 返回模式串
第0个字符P0在目标串中匹配的位置;
    如果在其中某个位置i: Ti!=Pi, 比较不相等, 这时可将模式串P右移一位, 用P中字
符从头与T中字符依次比较.

 /*==========================================================================* * @Description: * 朴素的模式匹配算法. * O(m*n) * * @Param t * @Param p * * @Returns: * *==========================================================================*/ int bf_find(const char *t, const char *p) { assert(t != NULL && p != NULL); int i, j; int nT = strlen(t); int nP = strlen(p); for ( i = 0; i <= nT - nP; i++ ) { for ( j = 0; j < nP; j++ ) if ( t[i+j] != p [j] ) break; if ( j == nP ) return i; } return -1; } 


2. KMP算法

BF算法的时间复杂度是O
(m*n), 这是因为有回溯, 但这些回溯是可以避免的, 也就是KMP

算法要做的事.

2.1 分析

设目标T=”
T0 T1 … Tn-1“, 模式P=”
P0 P1 … Pm-1“. 用BF算法做第s趟匹配比较时,

从目标T的第s个位置Ts与模式P的第0个位置P0开始比较, 直到在目标T第s+j位置Ts+j”

配”:

    T       T0  T1  …  Ts-1  Ts   Ts+1   Ts+2  …   Ts+j-1   Ts+j   …  Tn-1

                                          ||      ||         ||             ||            #

    P                                   P0   P1      P2       …   Pj-1       Pj

这时, 应有

            Ts Ts+1 Ts+2 … Ts+j-1 = P0 P1 P2 … Pj-1                
(1)

按BF算法, 下一趟应从目标T的第s+1个位置起用Ts+1与模式P中的P0对齐, 重新开始匹配

比较. 若想匹配, 必须满足:

        P0 P1 P2 … Pj-1 … Pm-1 = Ts+1 Ts+2 Ts+3 … Ts+j … Ts+m  
(A)

但, 如果在模式P中,

            P0 P1 … Pj-2 != P1 P2 .. Pj-1                            
(2)

则, 第s+1趟不用进行匹配比较, 就能断定它必然”
失配“; 即,
(A)不成立! 因为由
(1)


(2)式可知:

        P0 P1 … Pj-2 != Ts+1 Ts+2 … Ts+j-1
( = P1 P2 … Pj-1 )

同理, 若:

            P0 P1 … Pj-3 != P2 P3 … Pj-1

则:

        P0 P1 … Pj-3 != Ts+2 Ts+3 … Ts+j-1
( = P2 P3 … Pj-1 )

    … …

依此类推, 直到对于某一值k, 使得:

        P0 P1 … Pk    = Pj-k-1 Pj-k … Pj-1

且      P0 P1 … Pk+1 != Pj-k-2 Pj-k-1 … Pj-1

才有:

        P0  P1  …  Pk  =  Ts+j-k-1   Ts+j-k   …   Ts+j-1

                                      ||                ||                  ||

                                      Pj-k-1       Pj-k             Pj-1

这样, 我们可以把在第s趟比较”
失配“时的模式P从当时位置直接向右”
滑动” j-k-1

( = s+j-k-1 – s ) 位.

2.2 next特征函数

关于k的确定方法, Knuth等人发现, 对于不同的j, k的取值不同, 它仅依赖于模式P本身

前j个字符的构成, 与目标无关. 因此, 可以用一个next特征函数来确定: 当模式P中第j

个字符与目标S中的相应字符失配时, 模式P中应当由哪个字符与目标中刚失配的字符重新

继续进行比较.

    设模式 P = “
P0 P1 … Pm-2 Pm-1“, 由之前的分析可知, 它的next特征函数可以定

    义如下:

              /  -1,  当j=0

    next
(j) = |  k+1, 当0<=k<j-1且使得”
P0 P1 … Pk” = “
Pj-k-1 Pj-k … Pj-1“的最大整数

              \  0,   其它情况

2.3 实现

2.3.1 KMP

有了next值, KMP算法也就可以实现了.

 /*==========================================================================* * @Description: * KMP匹配算法. * O(m+n) * * @Param t * @Param p * * @Returns: *==========================================================================*/ int kmp_find(const char *t, const char *p) { assert(t != NULL && p != NULL); int nT = strlen(t); int nP = strlen(p); int posT = 0, posP = 0; int *next = NULL; if ( nP == 0 ) return 0; if ( NULL == (next = (int *)malloc(nP * sizeof(int))) ) return -1; kmp_ST_next(p, next, nP); while ( posP < nP && posT < nT ) { if ( posP == -1 || p[posP] == t[posT] ) { posP++; posT++; } else { posP = next[posP]; } } if ( next != NULL ) free(next); if ( posP < nP ) return -1; return posT - posP; } 


2.3.2 Next值

 static void kmp_ST_next(const char *p, int *next, int n) { assert(p != NULL && next != NULL && n > 0); assert(strlen(p) == n); int j = 0, k = -1; next[0] = -1; while ( j < n - 1 ) { if ( k == -1 || p[j] == p[k] ) { j++; k++; next[j] = k; } else { /* 此刻, 有: (k!=-1) 且 (P[j]!=P[k]) 且 (next[j]==k); * next[j]==(k-1)+1 ==> "Pj-k Pj-k+1 ... Pj-1" == "P0 P1 ... Pk-1" * * 即: 串"P0 P1 ... Pj"的next值为k, 而P[j]!=P[k], * 所以, 串"P0 P1 ... Pj+1"的next值(next[j+1])只可能出现在 * 串"P0 P1 ... Pk-1"中!! * * 由 "Pj-k Pj-k+1 ... Pj-1" == "P0 P1 ... Pk-1" 与 * "P0 P1 ... Pa" == "Pk-a-1 Pk-a ... Pk-1" (next[k]=a+1) * 得, "Pj-a-1 Pj-a ... Pj-1" == "P0 P1 ... Pa" * * 所以接下来只需要判断Pj是否等于Pa+1, 而Pa+1的下标即是next[k]! * 若相等, 则next[j+1]=next[k]+1; 若... * */ k = next[k]; } } } 


点赞