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