KMP算法解析

来源:互联网 发布:五笔vb是什么字 编辑:程序博客网 时间:2024/06/07 12:49

本文转载自【算法】KMP算法解析

KMP算法是一个很精妙的字符串算法,个人认为这个算法十分符合编程美学:十分简洁,而又极难理解。笔者算法学的很烂,所以接触到这个算法的时候也是一头雾水,去网上看各种帖子,发现写着各种KMP算法详解的转载帖子上面基本都会附上一句:“我也看的头晕”——这种诉苦声一片的错觉仿佛人生苦旅中找到知音,让我几乎放弃了这个算法的理解,准备把它直接记在脑海里了事。

但是后来在背了忘忘了背的反复过程中发现一个真理:任何对于算法的直接记忆都是徒劳无功的,基本上忘得比记的要快。后来看到刘未鹏先生的这篇文章:知其所以然(三):为什么算法这么难?才知道不去理解,而硬生生的背诵算法是多么困难的一件事情。因此我尽可能的尝试理解KMP的算法,并用自己的语言描述一下这个优雅算法的思维过程。

1. 明确问题

我们首先要明确,我们要做的事情是什么:给定字符串M和N(M.length >= N.length),请找出N在M中出现的匹配位置。说白了,就是一个简单的字符串匹配。或许你会说这项工作没什么难度啊,其实只要从头开始比较两个字符串对应字符相等与否,不相等就再从M的下一位开始比较就好了么。是的,这就是一个传统的思路,总结起来其思想如下:

1.当 m[j] === n[i] 时,i与j同时+1;
2.当 m[j] !== n[i] 时,j回溯到j-i+1,i回溯到0,然后回到第一步;
3.当 i === len(n) 时,说明匹配完成,输出一个匹配位置,之后回到第二步,查找下一个匹配点。

我们举个例子来演示一下这个比较的方法,给定字串M - abcdabcdabcde,找出N - abcde这个字符串。传统思路解法如下:

i: 0 1 2 3 4 5 6 7 8 9 0 1 2M: a b c d a b c d a b c d eN: a b c d e                 // 匹配四位成功后发现a、e不匹配i: 0 1 2 3 4 5 6 7 8 9 0 1 2M: a b c d a b c d a b c d eN:   a b c d e               // 发现 a、b不匹配i: 0 1 2 3 4 5 6 7 8 9 0 1 2M: a b c d a b c d a b c d eN:     a b c d e             // 发现 a、c不匹配i: 0 1 2 3 4 5 6 7 8 9 0 1 2M: a b c d a b c d a b c d eN:       a b c d e           // 发现 a、d不匹配i: 0 1 2 3 4 5 6 7 8 9 0 1 2M: a b c d a b c d a b c d eN:         a b c d e         // 匹配四位成功后发现a、e不匹配i: 0 1 2 3 4 5 6 7 8 9 0 1 2M: a b c d a b c d a b c d eN:           a b c d e       // 发现 a、b不匹配i: 0 1 2 3 4 5 6 7 8 9 0 1 2M: a b c d a b c d a b c d eN:             a b c d e     // 发现 a、c不匹配i: 0 1 2 3 4 5 6 7 8 9 0 1 2M: a b c d a b c d a b c d eN:               a b c d e   // 发现 a、d不匹配i: 0 1 2 3 4 5 6 7 8 9 0 1 2M: a b c d a b c d a b c d eN:                 a b c d e // 匹配成功

嗯,看起来蛮不错,匹配出了正确的结果。但是我们可以从N的角度上来看待一下这个匹配的过程:N串发现第一次的匹配其实挺完美的,就差一步就可以匹配到位了——结果第4位的a、e不匹配。这种功亏一篑的挫败感深深的影响了字符串N,指向它的指针不得不回到它的头部,开始与M的下一个字符匹配。“b不匹配、c不匹配、d不匹配……”这种感觉简直糟糕透了,直到N又发现一个a,继而又发现了接下来的b、c、d——这让N仿佛找到了第一次的感觉。可当指针走到第四位时,悲剧还是发生了。懊恼的N再次将指针指向自己的头部,开始与M的下一个字符进行匹配。“b不匹配、c不匹配、d不匹配……” N嘟囔着这句仿佛说过一遍的话,直到遇见了下一个a。这次N一点欣喜都没有,尽管匹配获得了成功,但是它总觉得上两次对它的打击实在是太大了。

“有没有什么改进的办法呢?如果一开始就没有产生匹配成功,只能下移一位进行重新匹配,这一点毋庸置疑。但是产生了部分匹配之后再发现不匹配,还需要再从头回溯吗?前两次的匹配我已经很努力的得出了匹配结果,难道因为一位的不匹配便要抛弃一切从头再来吗?”N努力思考着这个问题,然后回顾了一下刚才的匹配过程,“刚才在每一次回溯匹配的过程中,我都经历了b、c、d的不匹配,这是重复的啊!等等,b、c、d这三个字符好像很面熟啊,这……不是我本身吗?噢噢对的,因为之前我已经部分匹配成功了么,所以M中的这些字符肯定就和我本身匹配成功的那一部分是一样的啊,也就是说,如果产生了部分匹配成功,那么再次回溯就会和我本身进行比较;如果产生了多次部分匹配成功的情况,那就要多次与自己本身进行比较。这明显产生了冗余吗!”

能不能解决这个冗余呢?N想了一会儿,然后笃定的得出了一个结论:既然要多次比较自身,那不如先将自身比较一遍,得出比较结果保存起来,下次使用时直接调用就好了啊!

如果有读者跟不上字符串N的思路看的云里雾里,那么我就直接给出一个不难记住的结论好了:减少匹配冗余步数的精髓在于对字符串N进行预处理,通常我们把处理结果保存在一个叫做模式值(如果你看过别的文章,里面可能会有一个奇怪的看不懂的数组,那就是这个模式值数组了,又称作backtracking、Next[n]、T[n]、失效函数等等)的数组中。

2. 模式值数组与最长首尾匹配
可能有读者因上一节的匹配太缭乱而直接跳到这里,那笔者再重复一遍已经得到的结论:我们需要对字符串N进行预处理,得到一个叫做模式值数组的东西。那么我们怎样处理字符串N呢?

这个东西如果我们能思考出来,那我们就可以在KMP算法后面多写一个字母了(KMP算法是以其发现者Knuth, Morris, Pratt三人的名字首字母命名的)。我们首先感谢这三位大拿不辞辛劳的研究,然后直接给出这个处理的方法:寻找最长首尾匹配位置

这是什么意思呢?首尾匹配位置就是说,给定一个字符串N(长度为n,即N由N[0]…N[n]组成),找出是否存在这样的i,使得N[0]=N[n-i],N1=N[n-i-1],……,N[i]=N[n],不存在返回-1。如下图所示:

这里写图片描述

图中绿色的部分完全相等,满足首尾匹配。且不会找出一点k,k>i且满足N[0]=N[n-k],N1=N[n-k-1],……,N[k]=N[n]。我们假设确定最长首尾匹配的位置的函数为next,即 next(N[n])=i 当在匹配的过程中,发现N的j+1位不匹配时,回溯到第 next(N[j])+1 位来进行查找是最优的,换言之,next(N[j])+1 位是最早可能产生匹配的位置,之前的位都不可能产生匹配。证明如下:

证明匹配

我们设 next(N[j]) = e,则满足N[0...e] = N[j-e...j]。当N[j+1] != M[y+1]时,可知已经完成匹配:M[y-j...y] = N[0...j],则M[y-e...y] = N[j-e...j]。由此可以推知N[0...e] = M[y-e...y],即将N后移至首尾相等位置,仍然可以满足匹配,接下来只需要查看N[e+1]与M[y+1]是否相等即可。

这里写图片描述

证明最优

依然用反证法,假设存在f,f>e,满足N[0...f] = M[y-f...y],即其匹配位置出现在更早的位置,则由于M[y-j...y] = N[0...j],则M[y-f...y] = N[j-f...j],则满足N[j-f...j] = N[0...f],则e就不是最长的首尾匹配点,与原假设不符。因此e点时最早可能产生匹配的位置。如图所示:

这里写图片描述

3. 模式值数组的求取

我知道又有读者会直接跳到这一段——没关系,我们复述一下我们前两节得到的结论:一切的问题都归结于如何进行最长首尾匹配。我们把问题简化如下:对于给定的字符串N,如何返回其最长首尾匹配位置?如abca,返回0,表示第0位与最后一位匹配;abcab,返回1,表示N[0,1]=N[n-1,n];abc,返回-1,表示没有首尾匹配,等等。

简单的想一下这个问题,发现用递归求取是一个不错的办法。首先我们假设N[j]已经求出了next(next(N[0…j]) = i),那么对于N[j+1]的next应该怎么求呢?

三种情况:

N[j+1] == N[i+1]:这个情况十分的乐观,我们可以直接说next(N[0…j+1]) = i+1。至于证明则依然用反证法,可以很容易的得出这个结论。

N[j+1] != N[i+1]:这个情况就比较复杂,我们就需要循环查找i的next,即i = next(N[0…i]),之后再用N[j+1]与N[i+1]比较,知道其相等为止。我们依然用一张图来说明这个问题:

这里写图片描述

假设上图中k = next(i),那么我们说如果N[k+1] == N[j+1],那么k+1就是最长的首尾匹配位置,即next(N[j+1]) = k+1。你很快会发现这个证明模式与刚才的证明模式非常相同:首先我们证明其匹配,对于N[0…k]来说,其与N[i-k…i]匹配,同时由于N[0…i]与N[j-i…j]匹配,则N[i-k…i]与N[j-k…j]匹配,则N[0…k]与N[j-k…j]匹配。则如果N[k+1] == N[j+1],我们就可以说k+1是一个首尾匹配位置。如果要证明其实最长,那么可以依然用反证法,得出这个结论。
最后,如果未能发现相等,返回-1。证明新的字符串N[0…j+1]无法产生首尾匹配。

求next函数值的算法如下:

void  next(StringType  t , int next[]){  /*  求模式t的next串t函数值并保存在next数组中   */    int  k=1 , j=0 ; next[1]=0;    while (k<t.length){           if ((j==0)|| (t.str[k]==t.str[j])){               k++ ; j++ ;             if ( t.str[k]!=t.str[j] )              next[k]=j;            else              next[k]=next[j];        }        else             next[j]=j;           }    } }   

KMP算法如下

#define Max_Strlen 1024int next[Max_Strlen];int KMP_index (StringType  s , StringType  t){    /* 用KMP算法进行模式匹配,匹配返回位置,否则返回-1 */    /* 用静态存储方式保存字符串, s和t分别表示主串和模式串 */    int  k=0 , j=0 ;      /*初始匹配位置设置 */    while ((k<s.length)&&(j<t.length)){          if ((j==-1)||(s. str[k]==t.str[j])){            //j==-1表示未在模式串中找到匹配子串              k++ ;             j++ ;         }         else         j=next[j];     }    if(j >= t.length)          return(k-t.length) ;     else          return(-1) ;}    
0 0
原创粉丝点击