KMP算法

KMP简介

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)(来源:百度百科

简而言之,KMP名称取自于Knuth-Morris-Pratt,这是实现模式串和主串快速匹配算法,复杂度可以由暴力算法的 O ( m n ) O(mn) O(mn)降至 O ( m + n ) O(m+n) O(m+n)

KMP算法的思想,就是匹配失败的时候,主串指针不回溯,模式串指针返回指定位置
KMP算法中一个重要的准备步骤是,构建PMT(Patial Match Table),这个PMT给出了匹配失败是模式串指针返回的位置

构建PMT

PMT是与模式串长度相等的数组,将模式串从起始位置到当前位置的前缀子串成为S,则PMT这个位置的数值为S的真前缀子串和真后缀子串的最长共同子串的长度,例如,S=“abcab”,其真前缀子串为{“a”,“ab”,“abc”,“abca”},真后缀子串为{“b”,“ab”,“cab”,“bcab”},所以二者的最长共同子串就是"ab",则S所对应的PMT数值为2

对于模式串abababca,其对于的PMT为:
| 序号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|–|--|–|--|–|--|–|--|
| PMT | 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 |

寻找真前缀子串和真后缀子串的公共子串的思想,可以理解为,当某个位置模式串与主串匹配失败时,当前位置之前的、已匹配上的部分字串,如果既是模式串的真前缀字串,也是模式串的真后缀子串,那么就可以实现:主串指针不回溯,模式串指针后移数个位置,让模式串的真后缀子串与刚才提到的真前缀字串对应上,而模式串的当前位置就可以通过PMT表查出

由于当前位置匹配错误时,需要查看前一位置的PMT,表示的是前面可以保留多长的公共字串,如果前一个位置的PMT值为2,就表示前面保留的公共子串长度为2,所以当前位置从模式串的第3个位置开始匹配,第三个位置也刚好就是下标2,所以刚好等于前一个位置PMT的值

为了不要查询前一个位置,有些人会在PMT基础上构建next表

| 序号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|–|--|–|--|–|--|–|--|
| PMT | 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 |
| next|-1 | 0 | 0 | 1 | 2 | 3 | 4 | 0 |

next表其实就是PMT表整体进行右移一个单位,这样就能使得当前位置匹配错误,就查询当前位置的next表,以next表的值作为模式串的下标
存在一个特殊的情况是,如果当前位置是起始位置,也就是下标0,如果下标0仍然匹配错误,意味着主串指针不能保持不变,而是需要后移一个单位,因此常常将next表的起始位置设置为-1作为记号,利于判别这个特殊情况

PMT构建代码为:

int m = p.size();
int pmt[m];
// 因为只有一个字符,其真前缀子串和真后缀子串均为空,所以pmt第一个值为0
pmt[0] = 0;
// pmt采用自匹配模式,i为主串指针,j为模式串指针,从i=1开始匹配
for (int i=1, j=0; i<m; ++i){
	// 当j>=0且匹配不上,则如果j=0,赋值-1,如果j>0,则赋值前一位置的pmt
	// 当j=-1 或 j>=0且匹配成功时,退出循环
    while(j>=0 and p[i] != p[j]){
        j = j ? pmt[j-1] : -1;
    }
    // 增加判定,如果下一位置字符相同,则pmt值设定为之前相同位置的pmt值
    // 由于pmt的使用是下一位置匹配失败,则选择当前位置的pmt值作为j继续匹配
    // 例如ababab,如果此时位置是4,也就是第三个a,正常的话,pmt值为3
    // 也就是说,下一位的位置5的b匹配失败,则查询pmt,下一次从位置3继续匹配
    // 然而,我们发现,位置5和位置3都是b,也就是如果位置5匹配失败了,回溯到位置3也会失败
    // 因此,位置4的pmt取值就不能为3,也是取值与位置2(位置3前一位)的pmt相同即可
    // 换句话说就是,位置2回溯到哪,位置4就跟着回溯到哪
    if (j >= 0 && p[i + 1] == p[j + 1])
        pmt[i] = pmt[j++];
    else
    // pmt取值为j+1后的值,如果一直匹配成功,j会逐渐增大
        pmt[i] = ++j;
}

完整代码

bool kmp(string & s, string & p){
    int m = p.length(), n = s.length();
    int pmt[m];
    pmt[0] = 0;
    for (int i=1, j=0; i<m; ++i){
        while(j>=0 and p[i] != p[j]){
            j = j ? pmt[j-1] : -1;
        }
        if (j >= 0 && p[i + 1] == p[j + 1])
            pmt[i] = pmt[j++];
        else
            pmt[i] = ++j;
    }
    for (int i = 0, j = 0; i < s.length(); ++i){
    	// 匹配过程代码起始与pmt计算过程类似
    	// 匹配失败,则j回溯,最多回溯到-1,因为后面j自增,所以j还是从0开始
        while (s[i] != p[j] && j >= 0)
            j = j ? pmt[j - 1] : -1;
        // 如果前面匹配成功,或者j回溯到-1,则j进行自增
        ++j;
		// j取值等于p长度时,说明模式串全部匹配成功,算法结束
        if (j == p.length())
        {
            return true;
        }
    }
    // 如果前面没有return,说明未能达成匹配
    return false;
}

参考资料:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值