KMP是一个有关字符串匹配的算法,之前一直听过,但是从来没有好好学习过,今天有幸能和大神学习了KMP,心情异常之好,所以要写下学习记录和总结。
首先,描述一下KMP所要解决的问题:有一个字符串,我们称它为原串S,S的长度为n;现在,有一个字符串s,s的长度为k,要判断s是不是S的子串。那么比较朴素的算法就是暴力求解,即从S的第一个字符开始一次判断后面的k个字符和s是否相等,当然这里n是大于等于k的。这样看来时间复杂度应该是O(n*k)。那么如果是很多字符串的匹配的话,比如要判断1000个字符串是不是S的子串,那么就是1000*k*n这大的数据,如果n和k的值再大一些(原串一般在10^5左右,需要检测的字符串1000左右),那么这样下来,数量级应该是10^11。显然,在竞赛中是会超时的,就算平时的应用,也不是一个比较好的办法!
KMP,就是一个解决这个问题好办法!
那么现在概述述一下KMP的算法的思想(细节会在后面再详细解释):
首先是对于每个要检测的字符串,遍历一遍,找到字符串每个前缀自身最大的既是前缀也是后缀的子串,注意是每个前缀而不是字符串整体。然后显而易见,每个字符都可以代表一个前缀,即它之前的前缀。为每一个字符,配一个指针,指向这个字符串(即这个字符做代表的前缀)最大的既是前缀也是后缀的子串的最后一个字符。
然后开始将s和S进行比较。比较的过程是这样的,从两个字符的第一个字符开始进行匹配,按照朴素法如果失配(即相应字符不等),那么向后移一位,但是按照KMP的方法,因为之前的部分已经已经匹配过了,那么就可以根据之前匹配的信息以及该位置的指针来判断可以向右移一次移动多少位是能够保证正确匹配的,然后直接将s串向右移动相应的位数,继续比较。直达结束。
可以看出,这个匹配至少的时间复杂度是O(n+k)。
好理解了思想之后,就要来研究一下细节,我们从第一步开始研究,即如何在O(n)的时间内,求出s的每个字符的失配指针:
这有一点递推的思想,从左向右扫,根据前面的已经求得值来求解当前值。下面以一个例子来说明解决办法
-1 a s d a d a s s, 我们再字符串的前面给它赋值一个空值,用来表示既是最大前缀也是最大后缀的字符串的长度为0。为方便描述,在后面将最大的既是前缀也是后缀的字符串,称为前后缀。显而易见,s[1]的前后缀是空,即-1,s[2]的前后缀也是-1,s[3]的前后缀是-1,s[4]的前后缀是s[1],s[5]的是-1,s[6]的是1,s[7]的是2,s[8]的是-1。那么现在,从s[1]开始,很显然,前后缀是-1,那么s[2]的前后缀指针,应该是看s[2]前一个字符即s[1]的指针指向的位置的后一个字符设为s[x],看是否和s[2]相等,如果相等,那么就要指向s[x],如果s[x] != s[2] 那么就看s[x]对应的指针的位置的后一位,看是否和s[2]相等,一次类推,直到找到匹配的位置,或者找到了空,在这里,s[2]前一个位置的指针指的值就是-1,这个指针的后一位,是s[1]!=s[2],因此,s[2]的指针是空。可以继续模拟一下。
例如下面的: -1 a b c a a b 它对应的前后缀指针序列是 -1 0 0 0 1 1 2。要注意的是,我们称每一个字符的前后缀指针的下一位为失配指针,一旦沿着当前的前后缀指针走发现不匹配,不能就此停止,而是要继续沿着前后缀指针走,比如上述例子中,s[5]的前一位s[4]的前后缀指针指向了1,而s[2]!= s[5],这时我们要沿着s[1]的前后缀指针继续走,是0,而s[1]==s[5],那么s[5]的前后缀指针就被确定下来了,就是1。
那么失配指针就是,当发生不匹配的时候,要沿着失配指针转移到另一个状态。也就是说如果不匹配,程序应该往哪走,这是有失配指针决定的,而一个字符的失配指针就是前后缀指针的下一位!还要注意的是,走到了失配指针指向的地方,不一定能够匹配,如果不能匹配的话,就要沿着这个当前失配的位置的失配指针继续向前走,知道走到空或者找到匹配为止!
这个正确性是可以证明的,首先说找到失配位置x1的失配指针的位置,设为x2,那么x2之前的字符串是匹配过的,并且很重要的是x2之前的字符串是x2之前的字符串的前后缀,即前缀后缀相等,所以可以理解为将字符串向后移了一个距离是的前面的前后缀和后面的前后缀刚刚重合,已知之前后面的前后缀匹配成功,显然前面的移到这个位置也必然匹配成功。而且对于连续的不匹配来说,就要一直沿着失配指针走,这个也是正确的,因为每次沿着失配指针走,都相当于将s向后移动了一定的距离,使得失配指针所指的位置之前都是匹配成功的,那么仅仅是相同的步骤继续走而已,一样的道理,不明白的话可以自己动手模拟一下!
接下来就是如何匹配:
这个就比较好说了,明白了KMP的思想之后,这个问题就比较好解决。首先,从第一个字符开始匹配,直到发现了失配字符,这里设与失配字符相比较的S的字符是k,沿着失配字符的失配指针走,判断这个位置的字符和这个k是否匹配,如果匹配,就从这个字符开始继续向后比较;反之如果这个位置的后一位和k不匹配,那么就沿着这个位置的失配指针继续向前走,直到找到匹配的字符或者是空,即第一个字符和k比较,如果没有找到,也就说说到第一个字符的位置还是不匹配的话,就看k后面的字符和他们是否匹配和第一个字符匹配了,也就是要向后移动S的当前指针了,以此类推!
用比较形式化的语言来描述的话,就是如下代码(摘自博客 http://www.cnblogs.com/dolphin0520/archive/2011/08/24/2151846.html )
int KMPMatch(char *s,char *p) { int next[100]; int i,j; i=0; j=0; getNext(p,next); while(i<strlen(s)) { if(j==-1||s[i]==p[j]) { i++; j++; } else { j=next[j]; //消除了指针i的回溯 } if(j==strlen(p)) return i-strlen(p); } return -1; }
这里面的next[i]表示的是就是i的失配指针。
下面的代码是构造next指针:
void getNext(char *p,int *next) { int j,k; next[0]=-1; j=0; k=-1; while(j<strlen(p)-1) { if(k==-1||p[j]==p[k]) //匹配的情况下,p[j]==p[k] { j++; k++; next[j]=k; } else //p[j]!=p[k] k=next[k]; } }
以上的代码实现,是直接求的失配指针,和上面的KMP算法实现很类似的。即next[j] = k,指的是P[0...k-1] == p[j-k...j-1],所以说如果有p[j]==p[k],那么这个向后延了一位的子串的最长前后缀就是p[0...k] == p[j-k..j],也就是next[j+1] = next[j+1] = k+1; 如果不相等的话,那么可以看做是匹配不成功,k=next[k]。