KMP单模快速字符串匹配算法
KMP算法是由Knuth,Morris,Pratt共同提出的算法,专门用来解决模式串的匹配,无论目标序列和模式串是什么样子的,都可以在线性时间内完成,而且也不会发生退化,是一个非常优秀的算法,时间复杂度的上界是O(n+m)。
那么我们现在就来研究一下KMP算法究竟是个什么东西,我这里就尽量说的简洁一点,注重应用,原理的话还是需要通过练习来巩固,或者通过本文的参考链接继续深入地看。本文的很多思路都来自于参考。
1.为什么普通的字符串匹配算法会那么慢?
这个问题很好理解,暴力的字符串匹配算法,其实就是个组合的过程,一般来说有两个量,一个是i一个是j,i在目标串上移动,j在模式串上移动,如果target[i]==text[j],那么i++,j++,如果不等于,不好意思,请 i 回到开始匹配成功的下一个位置,j=0,重复匹配,可见这样的算法的复杂度是O(n*m),相当地慢,这种算法没什么技巧。
当然了如果学过数据结构的人都知道,对于字符串匹配还有比暴力法好得多的一种算法,那就是散列法,散列法通过算字符串的散列值来匹配模式串,我们只用把模式串散列一次然后就可以把维持m长度的指针从目标串的0位置匹配到n-m位置就可以了,这个算法的复杂度是上界是O(n),看上去很不错,但是事实上如果模式串很大的话,我们要找到一个很好的散列函数(第一个保证散列值都是唯一的(不可能用链表法,因为可能散列值会很大,重复很多效率就下降了),散列值最好是能从上一个推出下一个的),而且要开辟一个相当大的空间来储存散列值。
当然散列法也是一个很好的算法,用来算某些特定的问题会很快,但是有没有更好的呢?有,那就是KMP算法。
2.KMP算法的雏形
KMP算法的根本目的就是想让i不后退(散列法也是这样想的),这就要求我们把匹配过的信息进行储存,KMP算法用到一个next数组,是整个算法的关键。
讲next数组之前我们先来明白一下什么是模式串的前缀后缀最长公共长度,先来看一个表:
从这个表我们可以很清楚看到,所谓的前缀和后缀其实就是在第i个位置从前往后数和从pos位置从后往前数的一样的子串的长度,但是找到这个关系可以用来干嘛呢?
我们现在来看一下KMP算法的操作流程,再来看这个前缀和后缀与算法之前有什么关系:
KMP算法:
1.构建next数组
2.模式串与目标串进行匹配,假设现在模式串和目标串已经匹配到j和i,那么
如果target[i]==text[j],则i++,j++(这个和暴力算法一致)
如果target[i]!=text[j],则使j通过next数组跳转到前一个j(假设是k位置,的相当于是使j匹配从pos向前移动pos-j个位置,再判断target[i]是否等于text[k],
否则继续执行k=next[k],直到target[i]==text[k]。如果无法找到合适的元素使target[i]==text[k],则另k=0,且i++;
可以看到next数组是用来解决暴力解法的当匹配失败时就要j=0的缺点,可以让j跳转到某个合适的位置,继续匹配
当然了这个位置也不是随便乱选的,而这个位置就是刚好是当前元素前面元素的前缀后缀最长公共长度的后一个位置,当然了我们要要从当前位置跳转,我们关注的是当前位置前面的元素的情况,所以我们把上面那个表所有值往前移动一个单位,然后把0位置设置成-1就得到了next表了。
3.KMP算法的关于模式串移动的原理
大家看到这里可能会很疑惑,究竟为什么跳转到前面元素的前缀和后缀最长公共的后一个位置前面一个位置就是可以的呢?这个的确不太好说明,我们只从例子上说明问题,我们先来举一个例子:
从面例子中,其实移动到前注意和后缀最长公共位置是合理的,因为我们前后缀是相等的,我们移动后可以保证当前元素前面的元素都是匹配的,比如图中这个例子,移动以后AC还是和前面配过的元素一样,最后完成匹配,最后当然了,如果移动后还失配,还是需要相同的移动方法。直到移动到模式串的最前端为止(移动到最前端说明已经找不到任何匹配的元素了,相当于重新匹配模式串了)。
4.KMP算法的Next数组的代码构建
那么说了,我们最终的目的还是要想办法构建next数组,那么究竟怎么构建呢?
这里我们用到了递推法,通过前面的元素的结果来地推到当前的结果,其实这个递推的过程和KMP匹配的过程差不多
1.初始化Next[0]=-1,k=-1,i=0;
2.如果text[i]==text[k]或者k==-1,则i++,k++,Next[i]=k;
如果text[i]!=text[k],则使k通过next数组跳转到前一个k,,再判断text[i]是否等于text[k],
否则继续执行k=next[k],直到k==-1。如果无法找到合适的元素使text[i]==text[k]
代码很好写,如下:
1 typedef int Position;
2
3 void Get_Next(const string &str_text, int *const _next)
4 {
5 Position i = 0 , k = -1;
6 int slen = str_text.length();
7 _next[0] = -1;
8
9 while (i < slen)
10 {
11 if (k == -1 || str_text[i] == str_text[k])
12 {
13 k++;
14 i++;
15 _next[i] = k;
16
17 }
18 else k = _next[k];
19 }
20 }
思想和匹配的时候的是一样的,也是通过递推来得到最后前缀后缀的最长公共元素的长度,从而确定k究竟要跳到什么位置。
当然了,这里还可以优化一下,我们知道我们在KMP算法的时候如果pattern[i]和text[j]不相等的时候我们还是要往前跳转的,那么为什么我们不在next数组里面就完成这件事情呢?
1 typedef int Position;
2
3 void Get_Next(const string &str_text, int *const _next)
4 {
5 Position i = 0 , k = -1;
6 int slen = str_text.length();
7 _next[0] = -1;
8
9 while (i < slen)
10 {
11 if (k == -1 || str_text[i] == str_text[k])
12 {
13 k++;
14 i++;
15 _next[i] = str_text[i] != str_text[k] ? k : _next[k];
16 //当i和k相等,直接引用next[k],这样就可以避免重复配对,直接跳转到这个位上不重复的前缀上
17 }
18 else k = _next[k];
19 }
20 }
这样,我们就完成了优化,当KMP进行跳转的时候,我们将会用最小的步数跳到合适的位置。
5.KMP算法的模板
最后,我们根据之前KMP算法的原理,把KMP算法写出
1 typedef int Position;
2
3 bool KmpSearch(const string &,const string &,int *const);
4 void Get_Next(const string &,int *const);
5
6 bool KmpSearch(const string &pattern, const string &str_text, int *const _next)
7 {
8 Position i = 0, j = 0;
9 int plen = pattern.length(), slen = str_text.length();
10 Get_Next(str_text, _next);
11
12 while (i < plen &&j < slen)
13 {
14 if (j == -1 || pattern[i] == str_text[j])
15 {
16 //j==-1表示已经回溯到目标串的前部,最前部都不符合当前字符,i要前进一个单位
17 i++;
18 j++;
19 }
20 else j = _next[j];//直接跳转到最小的前缀去
21 }
22 return j == slen;
23 }
24
25 void Get_Next(string str_text, int *const _next)
26 {
27 Position i = 0 , k = -1;
28 int slen = str_text.length();
29 _next[0] = -1;
30
31 while (i < slen)
32 {
33 if (k == -1 || str_text[i] == str_text[k])
34 {
35 k++;
36 i++;
37 _next[i] = str_text[i] != str_text[k] ? k : _next[k];
38 //当i和k相等,直接引用next[k],这样就可以避免重复配对,直接跳转到这个位上不重复的前缀上
39 }
40 else k = _next[k];
41 }
42 }
最后关于KMP算法的时间复杂度的分析,我不作严格的证明,网上的很多文章也没有给出很严格的证明(计算机科学和数学密不可分,真正严格的证明应该是可以用数学符号来表示的,可是目前为止我看到的任何一篇文章都没有给出一个很严谨的证明),在《Fast Pattern Matching In Strings》——By Knuth-Morris-Pratt(事实上这篇论文是三位大神联合署名的,KMP算法的名字就是这三位大神名字首字母连起来)这篇论文有写,但是为了文章简洁一点还是不纠结了)。
我们来个形式上的推导:因为对于模式串的i,最多移动的位置是n,而对于j,最坏的情况是到最后一个元素才不匹配,j跳到字串的最开始,重新匹配,但是i又不向后移动,所以时间复杂度的上界是O(m+n)。
6.总结
KMP算法是一个优化非常强的算法,事实上KMP的Next跳转表是一个离散数学的重要工具:DFA(有限状态自动机,名字很高大上,其实就是一个跳转记录的东西)的一个简化版,事实上KMP是巧妙地根据字符串的前缀和后缀的联系来实现的,为字符串处理开辟了一个崭新的道路
参考:http://blog.csdn.net/joylnwang/article/details/6778316
http://blog.csdn.net/heiyeshuwu/article/details/42883293
http://blog.csdn.net/v_july_v/article/details/7041827
KMP算法广泛运用于ACM中,当然了工程中也可以运用,是很实用的算法,但是很有趣的是,除了比较的量很大的情况下,KMP的效率和C的内置函数strstr()的效率其实是差不多的,而strstr是直接拿两个字符串来比较的,算法复杂度是O(n*m),可能是一般比较的字串的长度都很小的缘故。
另外单模匹配算法除了KMP还有BM和Sunday算法,理论上后面两个算法的比KMP更优,不过其实应该也是快不了多少。