正文

我所理解的KMP算法(一)2009-05-10 22:03:00

【评论】 【打印】 【字体: 】 本文链接:http://blog.pfan.cn/goal00001111/43213.html

分享到:

我所理解的KMP算法                           作者:goal00001111(高粱)            始发于goal00001111的专栏;允许自由转载,但必须注明作者和出处   一.简单字符串模式匹配算法的缺陷 设有目标串T(target)和模式串P(pattern),模式匹配是指在目标串T中找到一个与模式串P相等的子串。模式匹配成功是指在目标串T中找到了一个模式串P。 简单字符串模式匹配算法(也就是BF算法)的基本思想是:从目标串T的起始比较位置pos开始(在后面的案例中,我们默认pos = 0),和模式串P的第一个字符比较之,若匹配,则继续逐个比较后继字符,否则从串T的下一个字符起再重新和串P的字符比较之。依此类推,直至串P中的每个字符依次和串T中的一个连续的字符序列(即匹配子串)相等,则称匹配成功,返回该匹配子串的首字符下标;否则成匹配不成功,返回-1。 BF算法的思想很直接,也很容易理解,其时间复杂度为O(lenT*lenP),其中lenT和lenP分别为串T和串P的长度。 我们先给出代码,再做简要分析: /* 函数名称:BFIndex 函数功能:简单字符串模式匹配算法,若目标串T中从下标pos起存在和模式串P相同的子串,则称匹配成功,返回第一个匹配子串首字符的下标;否则返回-1。 输入参数:const string & T :目标串T           const string & P :模式串P           int pos        :模式匹配起始位置 输出参数:int :匹配成功,返回第一个匹配子串首字符的下标;否则返回-1。 */ int BFIndex(const string & T, const string & P, int pos) {        int i = pos;        int j = 0;          while (i < T.size() && j < P.size()) {         if (T[i] == P[j]) //如果当前字符匹配,继续比较后继字符         {         ++i;             ++j;     }           else //否则i,j回溯,重新开始新的一轮比较         {               i = i - j + 1;               j = 0;                            //if (i > T.size() - P.size()) //一旦目标串剩余部分子串比模式串短,则无需再比较 //                      break;         }     }         if (j == P.size()) //匹配成功,返回第一个匹配子串首字符的下标     return i - j; else     return -1; }   我们发现,在某一轮比较中,一旦出现字符失配(即T[i] != P[j]),则需将i和j回溯,其中i回溯至i = i - j + 1,j回溯至j = 0。 这样产生了很多不必要的比较,例如(例1): string T = "aababaabaabc"; string P = "abaabc"; 在第4轮比较中,T3T4T5T6T7 == P0P1P2P3P4,我将其简写为T[3…7] == P[0…4](后面的都这样表示),但T[8] != P[5],出现字符失配,需要将i和j回溯,使得i = 4, j = 0。 而在第4轮比较中,我们已经得到了T[6…7] == P[3…4],又P[0…1] == P[3…4],相当于T[6…7]和P[0…1]已经间接地比较过,而且字符匹配了,我们无需进行从i =6, j = 0开始的重复比较。 实际上,当T[8] != P[5],即在i = 8, j = 5处出现字符失配时,我们无需将i回溯,只需将j回溯至failure[j](此时failure[5] = 2)处即可。即当T[8] != P[5]时,我们可以跳过比较T[6…7]和P[0…1](因为它们已经间接地比较过了),直接比较字符T[8]和P[2],这样可以省去很多不必要的回溯和比较,时间复杂度达到O(lenT+lenP)。这就是KMP算法的核心思想。   二.高效的KMP算法 现在继续剖析KMP算法。 我在上文提到当T[i] != P[j]时,我们无需将i回溯,只需将j回溯至failure[j]处即可。我们称failure[j]为模式串P下标j的失效函数。 失效函数的值failure[j]是指当T[i] != P[j]时,接下来与T[i]进行比较的模式串P的元素下标。如上面的例子,当T[8] != P[5]时,因为T[6…7] == P[3…4] == P[0…1],我们可以跳过比较T[6…7]和P[0…1],直接比较字符T[8]和P[2],所以failure[5] = 2。 如果你对失效函数还不太理解,我再举一些例子。 仍然以上面提供的目标串T和模式串S为例,当出现T[1…3] == P[0…2],但T[4] != P[3]时,若采用BF算法,则需要将i和j回溯,使得i = 2, j = 0。 而采用KMP算法,则无需将i回溯,j也不需要回溯至j = 0,而只需回溯至j = failure[j]。那如何得知failure[j]的值呢?观察模式串P,我们发现P[2] == P[0],因为T[3] == P[2],所以T[3] == P[0],相当于我们已经间接地比较过T[3] 和P[0]了,无需重复比较,接下来可以直接比较T[4]和P[1],所以failure[3] = 1。 再看一个简单的例子(例2): string T = "aabcabaababc"; string P = "ababc"; 当出现T[0] == P[0],但T[1] != P[1]时,要保证i不变,必须将j回溯至j = 0,然后比较T[1] 和P[0],所以failure[1] = 0。 同样的,当出现T[4…6] == P[0…2],但T[7] != P[3]时,要保证i不变,必须将j回溯至j = 0(为什么?好好想想!),然后比较T[7] 和P[0],所以failure[3] = 0。 那么,如果模式串P的第一个元素就不匹配,即T[i] != P[0]又该怎么办呢?j已经最小,没办法再往前回溯了,下一次比较必须使i自增1。这是一种特殊的情况,考虑到C语言中的数组下标从0开始,为了表示区别,我们设failure[0] = -1。很明显当failure[j] != -1时,在进行下一次比较之前,我们无需改变i的值;而当failure[j] == -1时,在进行下一次比较之前,必须先使i自增1。 我们继续分析例2,当出现T[1…2] == P[0…1],但T[3] != P[2]时,要保证i不变,必须将j回溯至j = 0,然后比较T[1] 和P[0]——看上去好像一切都顺理成章,但是请等等! 经比较T[1] == P[0],经观察P[2] == P[0],我们还有必要再去比较T[1] 和P[0]吗?当然不需要,我们应该直接比较T[2] 和P[0]才对!所以failure[2] = -1。 举了一大堆例子,苦于没有图象对照,想必各位看官已经看得头都大了!到底如何求模式串P的失效函数failure[j],可能很多人还是一头雾水(PS:我计划做一个教学视频,到时候图文声并茂,一定会帮助你理解的,记得随时关注博客,等待观看哦)。据考证,失效函数failure[j]是模式串P本身的属性,与目标串T无关,而且从不同的角度分析模式串P可以得到失效函数的不同表示方法。网络上此类文章可谓汗牛充栋,我的关于失效函数failure[j]的理解,与网友A_B_C_ABC 在其博文《KMP字符串模式匹配详解》(http://blog.csdn.net/A_B_C_ABC/archive/2005/11/25/536925.aspx)中所论述的“第一种表示方法”极为相似,如果你不想读我的文字,可以先去看A_B_C_ABC贴的图片,回过头再看我的文章,也许会明白我的意思——不会作图的下场啊!555 现在去A_B_C_ABC的博客看图! 。。。。。。 现在明白失效函数failure[j]的意义了吧?也应该知道如何求解failure[j]了吧? 总结一下吧: 先看失效函数的意义。 设在目标串T中查找模式串P,若T[i] != P[j],则将j回溯至失效函数值failure[j]处,那failure[j]可以取到哪些值呢? ① failure[0]= -1,表示T[i]和P[0]间接比较过了,且T[i] != P[0],接下来比较T[i+1]和P[0]; ② failure[j] = 0,表示比较过程中产生了不相等,接下来比较T[i]和P[0]; ③ failure[j] = k,其中0 < k < j,表示T[i]之前的k个字符与P中的前k个字符已经间接比较过了,且P[0…k-1] == P[j-k…j-1] == T[i-k…i-1],接下来比较T[i]和P[k]。 除了上述三种情况,failure[j]不可能取到其他值。        那么如何求解失效函数failure[j]的值呢?        从上述讨论可见,失效函数failure[j]的值仅取决于模式串P本身,与目标串T无关。 ① failure[0]= -1:考虑到C语言中的数组下标从0开始,模式串P的首字符的失效函数值规定为-1; ② failure[j] = -1:若P[j] == P[0],且P[0…k-1] != P[j-k…j-1],或P[0…k] == P[j-k…j],其中0 < k < j。 如:P = "abcaabcab"。 因为P[3] == P[0],且P[0] != P[1],P[0…1] != P[1…3],则failure[3] = -1; 又因为P[7] == P[0],且P[0…3] == P[4…7],则failure[7] = -1; ③ failure[j] = k:若P[0…k-1] == P[j-k…j-1],且P[k] != P[j],其中0 < k < j。 如:P = "abcaabcab"。 因为P[0] == P[3],且P[1] != P[4],则failure[4] = 1; 又因为P[0…3] == P[4…7],且P[4] != P[8],则failure[8] = 4; ④ failure[j] = 0:除(1)(2)(3)的其他情况。 如:P = "abcaabcab"中,failure[1] = failure[2] = failure[5] = failure[6] = 0;   算法思路: KMPIndex函数: KMP算法在形式上和BF算法即为相似。不同之处仅在于:当匹配过程中产生“失配”时,目标串T指示标志i不变,模式串P指示标志j回溯至failure[j]所指示的位置,并且当j回溯至最左端(即failure[j] == -1)时,使j = 0,i自增1,。 GetFailure函数: 根据failure[j]的定义,我们先规定failure[0] = -1;然后遍历模式串P,依次计算各个元素的失配函数值。 设已有k == 0;或0 < k < j,且P[0…k-1] == P[j-k…j-1];我们比较P[k]和 P[j]: 若P[k] == P[j],则由failure[j]的定义可知failure[j] = failure[k],之后k和j均自增1,继续比较后继字符; 若P[k] != P[j],则failure[j] = k。很明显之后不能直接比较后继字符;而需要将k回溯,直至找到使得P[0…k] == P[j-k…j]的最大k值,才可以让k和j均自增1,继续比较后继字符。 那么如何将k快速回溯到适当的位置呢? 我们设h = failure[k],很明显有:P[0…h-1] == P[k-h…k-1] == P[j-h…j-1]。 若P[h] == P[j],那h就是满足条件的最大k值。 若P[h] != P[j],则再在串P[0…h]中寻找更小的failure[h]。如此递推,有可能还需要以同样的方式再缩小寻找范围,直到failure[h] == -1才算失败。 若failure[h] == -1,则相当于k已经回溯到了模式串P的最左端,可以让k和j均自增1,继续比较后继字符。 实现代码如下: /* 函数名称:KMPIndex 函数功能:Knuth-Morris-Pratt算法,若目标串T中从下标pos起存在和模式串P相同的子串,则称匹配成功,返回第一个匹配子串首字符的下标;否则返回-1。 输入参数:const string & T :目标串T           const string & P :模式串P           int pos          :模式匹配起始位置 输出参数:无 返回值:int :匹配成功,返回第一个匹配子串首字符的下标;否则返回-1。 */ int KMPIndex(const string & T, const string & P, int pos) {        int *failure = new int[P.size()];               Getfailure(P, failure); //计算模式串P的失配函数failure[]               int i = pos;        int j = 0;          while (i < T.size() && j < P.size()) {         if (T[i] == P[j]) //如果当前字符匹配,继续比较后继字符         {         ++i;             ++j;     }           else  //否则保持i不变,将j回溯至failure[j],开始新的一轮比较         {               j = failure[j];               if (j == -1) //若j回溯至最左端,则使j = 0,i自增1                {             j = 0;             ++i;         }         }     }         delete []failure;         if (j == P.size()) //匹配成功,返回第一个匹配子串首字符的下标     return i - j; else     return -1; }   /* 函数名称:Getfailure 函数功能:计算模式串P的失配函数,并存入数组failure[] 输入参数:const string & P  :模式串P           int failure[]:模式串P的失配函数 输出参数:int failure[]:模式串P的失配函数 返回值:无 */ void Getfailure(const string & P, int failure[]) {        failure[0] = -1; //模式串P的首字符的失配函数值规定为-1        for (int j=1, k=0; j<P.size(); j++, k++)//遍历模式串P,计算失配函数值        {       //若P[0…k-1] == P[j-k…j-1],且P[k] == P[j],则failure[j] = failure[k],并继续比较后继字符       if (P[k] == P[j])       {             failure[j] = failure[k];         }         else //否则保持j不变,将k回溯至failure[k]         {               failure[j] = k; //若P[0…k-1] == P[j-k…j-1],且P[k] != P[j],则failure[j] = k               //寻找使得P[0…k] == P[j-k…j]的最大k值,才可以继续比较后继字符               while (k >= 0 && P[k] != P[j])//将k回溯至P[k] == P[j]或最左端,以进行下一轮比较                  k = failure[k];     } } //以下代码输出failure[i] for (int i=0; i<P.size(); i++)     cout << failure[i] << "   ";     cout << endl; }    我们刚才讨论的失效函数中有一个很巧妙的地方,那就是除了failure[0] = -1以外,模式串P中还有多处failure[j]可以等于-1,这就避免了重复比较T[i]和P[0],可以直接比较T[i+1]和P[0]。但是最初接触到这个算法的时候,我被这个“巧妙之处”足足折腾了半天——只因为效率上的一点点提高,却带来了理解上的巨大困难——真是得不偿失啊! 那么有没有更好理解的失效函数呢?当然有!  

阅读(3927) | 评论(1)


版权声明:编程爱好者网站为此博客服务提供商,如本文牵涉到版权问题,编程爱好者网站不承担相关责任,如有版权问题请直接与本文作者联系解决。谢谢!

评论

loading...
您需要登录后才能评论,请 登录 或者 注册