KMP字符串匹配算法

最近看了《算法(第4版)》(谢路云 译)上关于Knuth-Morris-Pratt(以下简称KMP)子字符串查找算法的介绍,看完之后不能清晰地理解算法的意思,特别是生成DFA二维数组那段代码,尽管只有短短数行,但却不明其义。几经思考,略有所解,现将一些想法记录下来。文中将待匹配的两字符串分别称为模式(pattern)文本(text),在文本中查找模式是否存在。

1. KMP算法相较于暴力匹配法的优势在于文本指针不用回退。以模式为ABACAC为例。


暴力匹配法,每当遇到匹配失败(红色)时,文本指针回退到开始匹配位置右边一位,在这个例子中,文本长度为16,一共比较了26次(绿色和红色,未着色部分未参与比较)。而在KMP算法里,匹配次数仅为文本长度N。KMP匹配时文本指针之所以不回退,是因为提前建立了一个状态表dfa[][],对于模式ABABAC来讲,DFA二维数组的内容如下


这个表怎么使用呢?第一列的A、B、C以及其他没列出来的字符都是指文本中的字符,第一行自然指模式中的字符,当文本中的字符X与模式中的第i个字符pat[i]比较时,则文本中的下一个字符与第dfa[X][i]比较。如文本中字符为A,与第0个比较,则下一个文本与dfa[A][0]=1比较;如果文本中字符为F,与第3个比较,因为dfa[F][0]=0,故下一个文本又与第0个比较。当dfa[][]的值为模式的长度时,匹配成功。由于文本的指针不会回退,故大大减少了匹配次数。

2. 理解KMP算法的一个典型示例。


如上图所示,在ABABABAC中查找ABABAC,当第一次匹配失败时,根据状态表dfa[B][5]=4,所以下一个文本字符A直接与pat[4]比较即可。这个表是怎么生成的呢?这里要引入一个“公共前后缀”的概念。在上图中,当匹配失败时,我们已经检查了ABABAB这一串字符,这一串字符的后缀与模式的前缀最大重合部分是ABAB,由于这一截与模式的前一截是一样的,就相当于模式的前面4个字符已经匹配到了,下一个文本字符与pat[4]比较即可。

3. 如何生成DFA状态表?

还是以模式ABABAC为例,可以肯定的是dfa[pat[j]][j] = j + 1. 即第j个匹配成功了,文本下一个字符匹配pat[j+1]。关键是匹配失败了,如何处理。当前缀AB到B匹配失败,说明文本为AX(X为代指,X不为B),当X不是A时,那么这段文本与模式的前缀无重合部分,故下一个文本字符与pat[0]比较;当X为A时,前后缀重合部分为1,下一个与pat[1]比较,即与dfa[][0]一致。

再如,当ABA匹配失败,说明文本为ABX(X不等于A),由于最长公共前后缀为0,故匹配失败的状态仍与dfa[][0]一致。

又如,当ABAB匹配失败,说明文本为ABAX(X不等于B),可以看到ABA的公共前后缀长度为1,说明在比较X与B时,上一个A已经与pat[0]匹配上了,所以在pat[3]处匹配失败的做法与在pat[1]处匹配失败的做法一致,即dfa[][3]=dfa[][1]。

由此可以推知dfa[][n]=dfa[][m],m为前n个字符的公共前后缀长度,而公共前后缀长度是在可以前面已有的dfa表里获得的,如ABA的公共前后缀长度1 = dfa[A][0],ABAB的公共前后缀长度为2 = dfa[B][1],ABABA的公共前后缀长度为3 = dfa[A][2]。由此可知,匹配失败时的状态可以递归地从前面的状态获得。

4. 代码实现。

KMP算法的Java实现在书上是现成的,我写了一份C++实现代码,在leetcode上提交成功了,代码如下

class Solution {
public:
    int strStr(string txt, string pat) {
        int Tlen = txt.length(), Plen = pat.length();
        if(txt == pat || Plen == 0) return 0;
        if (Tlen <= Plen || Tlen == 0) return -1;
       
        vector<vector<int>> dfa(Plen, vector<int>(256, 0));
	    dfa[0][(int)pat[0]] = 1;
	    for (int i = 1, X = 0; i < Plen; i++) {
		    dfa[i] = dfa[X];            
		    dfa[i][(int)pat[i]] = i + 1;
		    X = dfa[X][(int)pat[i]];
	    }
	    int i, j;
	    for (i = 0, j = 0; i < Tlen && j < Plen; i++) {
		    j = dfa[j][(int)txt[i]];
	    }
	    if (j == Plen) return i - Plen;
	    else return -1;
    }
};

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值