KMP算法

串的简单模式匹配

给出主串S和模式串T,要求找出T出现在S中的第一个位置。很容易想到的枚举法,就是从第一个字符开始,逐个比较S和T的字符,若相等就继续比较后续字符,否则从S当前与T匹配的第一个字符的下一个字符起再重新和T的第一个字符比较。即初始化 i = j = 0,若S[i] == T[j],则 i++; j++; 否则 i = i - j + 1; j = 0;

代码如下:

int Match(string const &S, string const &T)
{
    /* 找出T在S中出现的第一个位置,若不存在,则返回-1
     */
    int i = 0, j = 0, n = S.length(), m = T.length();
    while(i < n && j < m) {
        if(S[i] == T[j]) {i++; j++;}
        else {i = i - j + 1; j = 0;}   // 不相等,i和j都回溯
    }
    if(j == m) return i - m;
    else return -1;
}

方法虽然简单,且在一般情况下实际执行时间近似于O(m + n),但是它的时间复杂度是O(m * n)的,例如 S = “aaaaaaaaaaaaaaaaaaaaaaaaaab”,T = “aaaaaaaab”。

模式匹配的改进算法—KMP

KMP算法可以在O(m + n)的时间数量级上完成串的模式匹配操作。改进之处在于:每当一趟匹配过程中出现字符比较不等时,不需回溯i,而是利用已经得到的“部分匹配”的结果将模式串T向右“滑动”尽可能远的一段距离后,继续进行比较。

假设S = ‘s1s2…sn’, T = ‘p1p2…pm’,由上面的讨论可知,当匹配过程中产生“失配”(即si != pj)时,T应该向右“滑动”多远。换句话说,si应该与T中的哪个字符再比较?
假设此时应与第k(k < j)个字符比较,则T中的前k - 1个字符必须满足

'p1p2...p(k-1)' == 's(i-k+1)s(i-k+2)...s(i-1)'     (1)

而已经得到的“部分匹配”的结果是

'p(j-k+1)p(j-k+2)...p(j-1) ' == 's(i-k+1)s(i-k+2)...s(i-1)'     (2)

由(1)和(2)可推得下列等式

'p1p2...p(k-1)' == 's(i-k+1)s(i-k+2)...s(i-1)'     (3)

因此,若T中存在满足(3)式的两个子串,则S中的第i个字符与T中的第j个字符不等时,只需将T向右“滑动”至T[k],再与S[i]继续比较即可。因为此时’p1p2…p(k-1)’ == ‘s(i-k+1)s(i-k+2)…s(i-1)’。

若令next[j] = k表示当模式中的第j个字符与主串中的相应字符“失配”时,在模式串中重新和主串进行比较的字符的位置。则next函数的定义为 next[j] =

0      当 j == 1 时
max{ k | 1 < k < j 且 'p1...pk-1' == 'p(j-k+1)...p(j-1)'} 当此集合不空时
1      其它情况

例如:S = “acabaabaabcacaabc”, T = “abaabcac”,按上述next的定义可知

j         1 2 3 4 5 6 7 8
T         a b a a b c a c 
next      0 1 1 2 2 3 1 2

KMP的工作过程:i和j分别指向S和T中的待比较字符,初始化i = j = 1。若S[i] = T[j] ,则 i和j都自增1,否则i不变,而j退回到next[j]的位置再比较,若依旧不相等,j再退回到下一个next[j]的位置,直至j=0,i和j都自增1。

i        1 i 3 4 5 6 7 8 9
S        a c a b a a b a a b c a c a a b c
T        a b
j        1 j

此时,S[i] != T[j],j = next[2] = 1,i不变:

i       1 i 3 4 5 6 7 8 9
S       a c a b a a b a a b c a c a a b c
T         a 
j         j 

此时,S[i] != T[j],j = next[1] = 0,i和j都自增1,即i = 3, j = 1:

i       1 2 3 4 5 6 7 i 9
S       a c a b a a b a a b c a c a a b c
T           a b a a b c
j           1 2 3 4 5 j 

此时,S[i] != T[j],j = next[6] = 3,i不变:

i       1 2 3 4 5 6 7 8 9         i  
S       a c a b a a b a a b c a c a a b c
T                 a b a a b c a c
j                 1 2 3 4 5 6 7 8 j

此时,i = 14, j = 9 ,已经匹配得到答案。

KMP和本文一开始所述算法的不同之处在于:当匹配过程中产生“失配”时,指针i不变,指针j退回到next[j]所指示的位置上重新进行比较,并且当指针j退至0时,指针i和指针j需同时增1。即若主串的第i个字符和模式的第1个字符不等,应从主串的第i+1个字符起重新进行匹配。

代码如下:

/*
 * 上述讨论的是假设下标从1开始,这里从0开始
 * 找到就返回原下标的位置,否则返回-1
 */
int KMP(string const &S, string const &T, int *next)
{

     int i = 0, j = 0, m = T.length(), n = S.length();
     while(i < n && j < m) {
         if(-1 == j || S[i] == T[j]) {i++; j++;}
         else j = next[j];
     }
     if(j == m) return i - m;
     return -1;
}

KMP算法是在已知模式串的next函数值的基础上执行的,那么如何求得模式串的next函数值呢?
因为next函数值仅取决与模式串本身而和主串无关,所以可以从定义出发用递归的方法求出next函数的值:

 由定义可知:

    next[1] = 0

 设next[j] = k,则:

    'p1...p(k-1)' = 'p(j-k+1)...p(j-1)'

 此时next[j+1]的值有两种情况:

    若 k == 0 || pk == pj,则 next[j+1] = k + 1        (1)
    若 pk != pj,则 k = next[k],跳(1)               (2)             

代码如下:

void get_next(string const &T, int *next)
{
     next[0] = -1, next[1] = 0;
     int j = 1, m = T.length();
     int k = next[j];
     while(j < m) {
         if(-1 == k || T[k] == T[j]) next[++j] = ++k;
         else k = next[k];
     }
}

上述求得的next函数在某些情况尚有缺陷.例如:S = ‘aaabaaaab’,T = ‘aaaab’时,当 i = 4, j = 4时,S[i] != T[j].根据上述方法求得的next的指示,还需要进行i = 4, j = 3; i = 4, j = 2; i = 4; j = 1这3次比较.实际上,因为T中的前3个字符和第4个字符相等,所以应该将T一次向右滑动4个字符直接进行i = 4, j = 1时的字符比较

j                1 2 3 4 5
T                a a a a b
next             0 1 2 3 4
newnext          0 0 0 0 4

newnext的代码如下:

/*
 *下标从0开始
 */
void get_next(string const &T, int *next)
{
     next[0] = -1, next[1] = 0;
     int j = 1, m = T.length();
     int k = next[j], tmp = 0;
     while(j < m) {
         if(-1 == k || T[k] == T[j]) {
             if(-1 == k) next[++j] = ++k;
             else if(T[k+1] != T[j+1]) {k = tmp + 1; next[++j] = k; tmp = 0;}
             else {++tmp; next[++j] = k;}
         }
         else k = next[k];
     }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值