算法:KMP算法
算法分析
KMP算法是一种快速的模式匹配算法。KMP是三位大师:D.E.Knuth、J.H.Morris和V.R.Pratt同时发现的,所以取首字母组成KMP。
少部分图片来自孤~影的原创文章。
next函数的求解来自唐小喵的原创文章。(http://www.cnblogs.com/tangzhengyue/p/4315393.html)
朴素的模式匹配算法,也就是我们都比较直观接收的思路是:
从主串和模式串的第一个字符开始比较
直到遇到两个不一样的。然后我们拿让模式串回到第一位再和主串的下一位比较,然后知道匹配成功。这种算法,一旦匹配失败,不管已经匹配了多少,直接忽略从头再来,即i,j都归为,再让i移向下一位,重新开始匹配。
朴素算法没有任何问题,但是不高效。我们今天介绍的KMP算法,做到了使i不归位,让模式串移动。
(图A)
比如匹配到i=3,j=3的时候失败,这时,我们不让i归位,我们让模式串往右边移动至下图位置。
(图B)
(图C)
为什么要这样跳跃式的移动呢?我们来分析一下。
在第一张图中,a和c匹配失败,在保证i不变的清空下,i是不是应该和j前面的a和b比比看呢?
很显然,a只能和模式串的第一位进行比较,因为他和b对齐的话,就要保证i=2上的b和j=1上的a一样,很显然这是不可能的。所以它只能和模式串的第一位比。
在第二次失配时,见图B.。也是在保证i位置不变的情况下,只能让i=7和j前面的字符进行比较,如下图。
但是此时,它不需要直接和模式串的第一位进行比较了,因为我们发现,他和第二位b比较的时候,既可以保证i=6和j=1是适配的,所以它可以跳过模式串的第一位直接和第二位比较,恰巧都是b也是适配的。
但是因为i=7之前和j=5之前,是适配成功的,也即是说j=6的a就是j=4的a,,见上图。
所以其实我们是发现了一个规律,但是还很模糊,也就是说。
如果失配之前的模式串中存在前缀和后缀一样的,比如上图中模式串的前缀和后缀都有a,就说明主串A不用再和模式串中的第一位进行比较,可以直接和第二位B进行比较。
所以我们的我们现在的重心就是计算各个位置前的模式串的最长相同前后缀的长度。比如上面长度为1,我们就可以直接和第2为比,越过第一位。
我们建立数组next就是用来存储模式串中各个位置失配时需要移动到的位置(里面存储的实际上就是最长相同前后缀的长度。)
(来自唐小喵)
KMP的next数组求法是很不容易搞清楚的一部分,也是最重要的一部分。我这篇文章就以我自己的感悟来慢慢推导一下吧!保证你看完过后是知其然,也知其所以然。
下面我们就来说说KMP的next数组求法。
KMP的next数组简单来说,就是保证i永远不回退,只回退j来使得匹配效率有所提升。它用的方法就是利用strKey在失配的j为之前的成功匹配的子串的特征来寻找j应该回退的位置。而这个子串的特征就是前后缀的相同程度。
所以next数组其实就是查找strKey中每一位前面的子串的前后缀有多少位匹配,从而决定j失配时应该回退到哪个位置。
我知道上面那段废话很难懂,下面我们看一个彩图:
这个图画的就是strKey这个要查找的关键字字符串。假设我们有一个空的next数组,我们的工作就是要在这个next数组中填值。
下面我们用数学归纳法来解决这个填值的问题。
这里我们借鉴数学归纳法的三个步骤(或者说是动态规划?):
1、初始状态
2、假设第j位以及第j位之前的我们都填完了
3、推论第j+1位该怎么填
初始状态我们稍后再说,我们这里直接假设第j位以及第j位之前的我们都填完了。也就是说,从上图来看,我们有如下已知条件:
next[j] == k;
next[k] == 绿色色块所在的索引;
next[绿色色块所在的索引] == 黄色色块所在的索引;
这里要做一个说明:图上的色块大小是一样的(没骗我?好吧,请忽略色块大小,色块只是代表数组中的一位)。
我们来看下面一个图,可以得到更多的信息:
1.由"next[j] == k;"这个条件,我们可以得到A1子串 == A2子串(根据next数组的定义,前后缀那个)。
2.由"next[k] == 绿色色块所在的索引;"这个条件,我们可以得到B1子串 == B2子串。
3.由"next[绿色色块所在的索引] == 黄色色块所在的索引;"这个条件,我们可以得到C1子串 == C2子串。
4.由1和2(A1 == A2,B1 == B2)可以得到B1 == B2 == B3。
5.由2和3(B1 == B2, C1 == C2)可以得到C1 == C2 == C3。
6.B2 == B3可以得到C3 == C4 == C1 == C2
上面这个就是很简单的几何数学,仔细看看都能看懂的。我这里用相同颜色的线段表示完全相同的子数组,方便观察。
接下来,我们开始用上面得到的条件来推导如果第j+1位失配时,我们应该填写next[j+1]为多少?
next[j+1]即是找strKey从0到j这个子串的最大前后缀:
#:(#:在这里是个标记,后面会用)我们已知A1 == A2,那么A1和A2分别往后增加一个字符后是否还相等呢?我们得分情况讨论:
(1)如果str[k] == str[j],很明显,我们的next[j+1]就直接等于k+1。
用代码来写就是next[++j] = ++k;
(2)如果str[k] != str[j],那么我们只能从已知的,除了A1,A2之外,最长的B1,B3这个前后缀来做文章了。
那么B1和B3分别往后增加一个字符后是否还相等呢?
由于next[k] == 绿色色块所在的索引,我们先让k = next[k],把k挪到绿色色块的位置,这样我们就可以递归调用"#:"标记处的逻辑了。
由于j+1位之前的next数组我们都是假设已经求出来了的,因此,上面这个递归总会结束,从而得到next[j+1]的值。
我们唯一欠缺的就是初始条件了:
next[0] = -1, k = -1, j = 0
另外有个特殊情况是k为-1时,不能继续递归了,此时next[j+1]应该等于0,即把j回退到首位。
即 next[j+1] = 0; 也可以写成next[++j] = ++k;
代码
#include <stdio.h> #include <string.h> #define N 100 int kmp(char * str,int slen,char * ptr,int plen,int * next) { int s_i=0; int p_i=0; while(s_i<slen && p_i<plen) { if(str[s_i]==ptr[p_i]) { s_i++;p_i++; } else { if(p_i==0) s_i++; else p_i=next[p_i-1]+1; } } return ( p_i == plen ) ? ( s_i - plen ) : -1; } void cal_next(char *str,int *next,int len) { int j=0; int k=-1; next[0]=-1; while(j<len-1) { if(k==-1||str[j]==str[k]) //如果我们 k==-1,我们直接回到第一个字符 { ++j;++k;next[j]=k; } else { k=next[k]; } } } int main(int argc, char **argv) { char str[ N ] = {0}; char ptr[ N ] = {0}; int slen, plen; int next[5]; scanf( "%s%s", str,ptr); plen = strlen(ptr); slen=strlen(str); cal_next( ptr, next, plen ); for(int i=0;i<plen;i++) printf("%d",next[i]); printf("[%d]",kmp(str,slen,ptr,plen,next)); return 0; }