<<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]; } } }