字符串匹配算法之KMP算法


第一次写技术博客,选择写KMP算法的原因是这是我接触比较久的一个算法,因为比较抽象,之前一直没弄懂它,后来参阅了相关的资料,似乎懂了点不久就又忘没了,终于在参阅了《算法导论》及各大技术博客之后,想把自己对KMP算法的理解写下来。

字符串匹配问题形式化定义如下:假设文本是一个长度为n的数组T [1…n],而模式是一个长度为m的数组P[1…m],其中m<=n,P 和T 都是来自一个有限字母集的字符。如果模式P在文本T中出现,且偏移为s,则称s为一个有效偏移,否则成为无效偏移。字符串匹配问题就是找到模式P在文本T中的所有有效偏移。下图即是一个字符串匹配的例子。
字符串匹配的一个例子

算法思想

这里写图片描述
我们考察一下朴素字符串匹配算法的操作过程,上图展示了一个文本T与模式P匹配的过程,在这个例子中,q=5个字符已经匹配成功,但模式的第六个字符不能与相应位置的文本字符匹配,可见偏移s=4是一个无效偏移,这时一个很自然的想法是令偏移加1,即s=4+1,然而实际上这仍然是一个无效偏移,因为模式P的前四个字符abab与文本T中相应位置的子串baba不相同。细心的你会发现这四个字符包含在已经匹配成功的q个字符中,如果我们简单地在偏移s无效的情况下直接考察偏移s+1,显然我们就忽视了已经匹配成功的前q个字符可能给我们带来的某些有用的信息,所造成的后果就是带来了较高的时间复杂度O( n ∗ m n * m nm)。实际上,q个字符已经匹配成功的信息确定了相应的文本字符,而这q个已知的文本字符使我们能够立即确定某些偏移是无效的,如偏移s=4+1是无效的,偏移s=4+2 可能是有效的。那么这q个字符是如何给我们提供这样信息的呢?
KMP算法思想
如上图所示,当匹配到第q+1个字符时两个字符不相等,所以偏移s是无效偏移,这时需要将模式串P向前移动,那么移动到哪里合适呢?下一个要考察的偏移可以是 s ′ = s + 1 s'=s+1 s=s+1,也可是是 s ′ = s + q s'=s+q s=s+q,或者两者之间,但不可以 s ′ > s + q s'>s+q s>s+q(这是显然的),所以我们要在 s+1~s+q之间找下一个可能的有效偏移,即上文提到的“合适”的位置。如上图第三行,当偏移s无效时,模式串P向前移动到偏移s’,显然偏移s’是有效偏移的必要条件是A=B,如果A!=B,那么s’一定无效偏移,所以我们要找的“合适”的位置,是向前移动模式串P到第一个满足A=B的位置,而这个位置就是A和B是P[1…q]的最长公共前后缀不算该串本身)的位置,我们表示为next[q],当知道了这个最长公共前后缀的长度,就可知直接将模式串向前移动 q − n e x t [ q ] q-next[q] qnext[q] 位,接着继续比较下一个位置。然后我们便可以直接从文本T中的“a”开始比较,即文本T中已经比较过的位置不需要在再比较,算法的复杂性显然是 O ( n ) O(n) O(n),这就是KMP算法的思想。了解了KMP的思想,我们知道首先要做的就是先求出模式穿P每个位置的最长公共前后缀长度,后面我们会看到该过程的时间复杂度是 O ( m ) O(m) O(m),所以KMP算法的时间复杂度是 O ( n + m ) O(n+m) O(n+m)

计算next数组

长度为1的字符串的最长公共前后缀的长度为0,即 n e x t [ 1 ] = 0 next[1]=0 next[1]=0假设我们已经求得next[1],next[2],,,next[q],分别表示长度为1到i的字符串的最长公共长度,现在要求next[q+1]。
最长公共前后缀
如上图所示,假设A和B是P[1…q]的最长公共前后缀(即A=B),如果a=b,则显然 n e x t [ q + 1 ] = n e x t [ q ] + 1 next[q+1]=next[q]+1 next[q+1]=next[q]+1;如果 a ≠ b a \neq b a=b 怎么办呢,我们可以考察串A的最长公共前后缀A1和A2,因为 A 1 = A 2 A1=A2 A1=A2 B 1 = B 2 B1=B2 B1=B2 A = B A=B A=B,所以 A 1 = B 2 A1=B2 A1=B2,因此如果此时A1的下一个字符等于b,那么next[q+1]=next[ next[q] ]+1,如果不相等,我们就继续考察A1的公共前后缀,直到不能分割为止。如果分割到不能再分,那么next[q+1]显然为0了。
求next数组的代码如下:

const int MAXN = 100;
//next[x]表示前x个字符的最长公共前后缀。
//显然下标从1开始是有意义的,为了方便我们置next[0]=0。
int next[MAXN];
void getNext(string str) {
    next[0] = next[1] = 0;
    int comLength = 0;
    //i为字符串下标,字符串编号从0开始。
    for(int i = 1; i < str.size(); i++) {
        //设str前i个字符为substr
        //如果substr存在公共前后缀(comLength > 0),
        //并且前缀的下一个字符不等于substr的下一个字符(str[i] != str[comLength])
        while(comLength > 0 && str[i] != str[comLength]) {
            //更新最长公共前后缀
            comLength = next[comLength];
        }
        if(str[i] == str[comLength]) {
            comLength++;
        }//else comLength一定是0
        //计算str[0-i]子串的最长公共前后缀
        next[i + 1] = comLength;
    }
}

KMP算法代码

有了求next数组的代码,我们接下来给出KMP算法代码如下,我么可以看到,模式串P的前移只是概念上的前移,只要我们在比较的时候从最大公共长度之后比较P和T即可达到模式串P前移的目的。
注:上文的算法描述中next数组和字符串的下表都是从1开始,代码中next数组下标仍然从1开始,但是字符串下标出于习惯从0开始,希望不会引起不必要的混淆。

//pattern在text中出现的位置
void kmp(string text, string pattern) {
    //q为已匹配的字符串长度
    int q = 0;
    //遍历text
    for(int i = 0; i < text.size(); i++) {
        while(q > 0 && text[i] != pattern[q]) {
            q = next[q];//下一个字符不匹配
        }
        if(text[i] == pattern[q]) {
            q++;//下一个字符匹配
        }
        if(q == pattern.size()) {//整个pattern都匹配了
            cout << "Pattern occurs with shift " << i - q + 1 << endl;
            q = next[q];//寻找下一次匹配
        }
    }
}

int main() {
    string text, pattern;
    cin >> text >> pattern;
    getNext(pattern);
    kmp(text, pattern);
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值