C#编程之经典算法——查找(五)

KMP法匹配

 

      通过之前的讲解,我们了解了如何从一个数组中查找单个元素,而这一篇,让我们来一起学习下如何快速地从一个数组A中找出与另一个数组B完全匹配的连续的元素(即B为A的子集)。

      首先,我们来看下面的例子。

      string A = “abcdabcaba”; //在这里,我们把string当作char[]

      string B = “abca”;

      int i = 0; //index of A

      int j = 0;//index of B

方法一:

     我们来找出B在A中的位置,按照正常的思路,我们将按如下步骤来进行匹配

     1.将A[i]与B[j]比较,如果相等再比较A[i+1]与B[j+1],然后再比较A[i+2]与B[j+2]……A[i+n]与B[j+n]

        (i+n < A.Length && j+n < B.Length)。

     2.如果A[i]与B[j]不相等,i = i+1, j = 0,然后重复步骤1。

     3.重复步骤1,2,直到A.Length – i < B.Length。

     这种方法比较好理解,示例代码如下:

 /// <summary> /// 查找子串B在串A中完全匹配的开始索引 /// </summary> /// <param name=”A”>串A</param> /// <param name=”B”>串B</param> /// <returns>返回串B在串A中的开始索引,如果无匹配则返回-1。</returns> int IndexOf(string A, string B) { int i = 0; // index of A, 从A的起始处开始匹配,如果有给定的起始索pos,则 i = pos。 int j = 0; // index of B while (A.Length – i >= B.Length) { for (j = 0; j < B.Length; j++) { if (A[i + j] != B[j]) { i++; break; } else { if (j == B.Length – 1) { return i; } } } } return -1; }

方法二:

      虽然方法一很好理解,代码也很简单,但这种方法很耗时,有没有更快一点的方法呢?当然有,不然写方法二干嘛(^_^)。比较常用的算法是KMP算法。

      KMP算法是一种改进的字符串匹配算法,由D.E.Knuth与V.R.Pratt和J.H.Morris同时发现,因此人们称它为克努特——莫里斯——普拉操作(简称KMP算法)。KMP算法的关键是根据给定的模式串W1,m,定义一个next函数。next函数包含了模式串本身局部匹配的信息。

      KMP算法本身是不难理解的,关键就是这个next函数的定义。

      要说清楚KMP算法,可以从朴素的模式匹配算法说起,也就是方法一,不过为了方便学习KMP算法,我们重写一下方法一的示例。

/// <summary> /// 查找子串B在串A中完全匹配的开始索引 /// </summary> /// <param name=”A”>串A</param> /// <param name=”B”>串B</param> /// <returns>返回串B在串A中的开始索引,如果无匹配则返回-1。</returns> int IndexOf(string A, string B) { int i = 0;//从A的起始处开始匹配,如果有给定的起始索pos,则 i = pos。 int j = 0; while (i < A.Length || j < B.Length) { if (A[i] == B[j]) { i++; j++; } else { i = i – j + 1; j = 0; } } if (j == B.Length) //如果匹配成功则j的值为B.Length { return i – j; } else { return -1; } }

 

 

      还是上面的两个字符串string A和string B,当我们第一比较到第四个字符时,发现A[3] != B[3],那我们是不是可以跳过多个已比较的字符(最好是比较过的全部跳过)再开始比较呢?从上面示例中,我们可以得到A与B的关系为B[0]B[1]B[2]B[3]…B[j – 1] = A[i – j ]A[i – j + 1]A[i – j + 2]A[i – j + 3]…A[i – 1]。

      可见,在朴素的模式匹配算法中,当模式中的B[j]与主串中的A[i]不匹配时,需要把主串的指针(索引)回溯到i-j+1的地方从新用A[i-j+1]跟B[0]进行匹配比较。KMP算法的想法是,能不能不回溯主串的指针呢?这种想法基于如下事实的:B[j]!=A[i]前,B[0]~B[j-1]跟A[i-j]~A[i-1]是匹配的(这里j>0,也就是说在不匹配前已经有匹配的字符了。否则如果j=0,则主串指针肯定不用回溯,直接向前变成i+1再跟p[0]比较就是了)。

      我们通过例子来看一开始的比较。A[0]A[1]A[2] = B[0]B[1]B[2],但A[3] != B[3],且B[0]!=[B1] != [2],B[3] = B[0]。因为已经比较到了串B的最后一个字符了,所以i = i +1,然后再进行下一次比较

      1.因为A[1] = B[1],B[1] != B[0],所以A[1] != B[0],所以A[1]A[2]A[3]A[4] != B[0]B[1]B[2]B[3],所以跳过一步,i = i + 1;

      2.因为A[2] = B[2],B[2] != B[0],所以A[2] != B[0],所以A[2]A[3]A[4]A[5] != B[0]B[1]B[2]B[3],所以跳过一步,i = i + 1;

      3.因为A[3] != B[3],B[3] = B[0],所以A[3] != B[0],同上,i = i + 1;

 

再来看这两个字符串

 

string A = “aaaaaaab”; //在这里,我们把string当作char[]

string B = “aab”;

 

      我们还像上面那样分步比较。

      A[0]A[1] = B[0][B1],但A[2] != B[2],且B[0] = B[1],B[2] != B[1],B[2] != B[0]。 i = i + 1.

      1.因为A[1] = B[1],B[1] = B[0],所以A[1] = B[0]。又因为A[2] != B[2],B[2] != B[1],所以无法证明A[2]是否等于B[1],所以无法证明A[1]A[2]是否等于B[0]B[1]。所以 i 的值不变,但下次比较时,可以直接比较A[2]与B[1],即 j = 1;

 

 

再来看两个字符串

 

string A = “abababacabad”; //在这里,我们把string当作char[]

string B = “abac”;

 

      还是一样比较。我们得到

       A[0]A[1][2] = B[0][B1][2],但A[3] != B[3],且B[0] != B[1],B[2] != B[1],B[2] != B[0],B[3] != B[2] != B[1] != [0]。i = i + 1.

      1.因为A[1] = B[1],B[1] != B[0],所以A[1] != B[0],所以A[1]A[2]A[3]A[4] != B[0]B[1]B[2]B[3],所以这一步跳过。i = i + 1.

      2.因为A[2] = B[2],B[2] = B[0],B[2] = B[0],所以A[2] = B[0],又因为A[3] != B[3],B[3] != B[1],所以无法证明A[3] 与 B[1]是否相等,也就无法证明A[2]A[3]与B[0]B[1]的关系,所以 i 不值, j = 1. 然后经证明A[3]也B[1]是相等的,所以再继续比较A[4]与B[3],证明也是相等的,再比较A[5]与B[4],经证明不相等。所以 i = i + 1.

      3.重复步骤1,2直至得到结果。

 

      通过上面的例子,我们发现,有一些比较过程已经能通过人为的分析而得出结果,所以就没必要再让计算机去比较了,但使用这个方法,我们需要知道的是,每次比较需要跳过几个字符,并且再次比较时,直接比较第几个字符。我们假设,在B[j] != A[i]前共有k个字符是相等的。则可以知道A[i-k]~A[i-1]跟B[0]~B[k-1]是匹配的,那么,A[i]只需要跟B[k]进行比较就行了。而这个k是跟主串无关的,只需要分析模式串就可以求出来(这就是普通的教材中next[j]=k这个假设的由来,普通教材中总喜欢假设这个k值已经有了,如果你逻辑思维强还没有什么,不然或多或少会把你卡在这的)。亦即next[j]=k。

      如果k = 0时,则意味着B[j]前的串中不存在B[0]…=…B[j-1]的情况,就连B[0]也不等于B[j-1],也就是说B[0]~B[j-1]中所有以B[j-1]为结尾的子串跟模式B都是失配的。基于上面B[0]~B[j-1]=A[i-j]~A[i-1]的事实,可以断定A[i-j]~A[i-1]中所有以A[i-1]结尾的子串跟模式B都是失配,这说明把主串的指针回溯到i-j+1~i-1都是没有必要的,既然没有必要回溯,而A[i]!=B[j],则A[i]只能跟B[0]进行比较匹配了。亦即next[j]=0。

      还有一种情况,j=0,即A[i]!=B[0],这时不用再用A[i]来跟B中的其它字符比较了,变成用A[i+1]跟B[0]进行比较。为了统一,可以让next[0]=-1。在下一轮的比较中,判断到j=-1的情况时,让i=i+1,j=j+1,自然就形成A[i+1]跟B[0]比较的效果了。
      我们还看一下next函数的定义:

      1.next[0]= -1 意义:任何串的第一个字符的模式值规定为-1。
      2.next[j]= -1 意义:模式串T中下标为j的字符,如果与首字符
相同,且j的前面的1—k个字符与开头的1—k
个字符不等(或者相等但T[k]==T[j])(1≤k<j)。
如:T=”abCabCad” 则 next[6]=-1,因T[3]=T[6]
      3.next[j]=k 意义:模式串T中下标为j的字符,如果j的前面k个
字符与开头的k个字符相等,且T[j] != T[k] (1≤k<j)。
即T[0]T[1]T[2]。。。T[k-1]==
T[j-k]T[j-k+1]T[j-k+2]…T[j-1]
且T[j] != T[k].(1≤k<j);
      4. next[j]=0 意义:除(1)(2)(3)的其他情况。

next函数代码

int[] GetNext(string T) { if (T.Length == 0) { return new int[0]; } int[] next = new int[T.Length]; int i = 0; int j = -1; next[i] = -1; //定义1 while (i < T.Length) { //如果没有发现相同的字符,在向前查找,直至j = -1 //如果j = -1则下一个字符T[i]与T[0]比较 //这样可以得定义2,3和定义4 if (j != -1 && T[i] != T[j]) { j = next[j]; } else { ++i; ++j; //如果发了相同的字符,则 T[next[i]] = T[next[j]] //所以将next[i]的值设为next[j] //如果j=0,即T[i]与首字母相同,那么next[i] = next[0],即next[i] = -1,得到定义2。否则为定义3 if (i < T.Length && T[i] == T[j]) { next[i] = next[j]; } else { //如果j = 0,得到定义4,否则为定义3。 if (i < T.Length) { next[i] = j; } } } } return next; }

KMP代码示例

int IndexOf(string S, string T) { int[] next = GetNext(T); int i = 0; //index of S int j = 0; //index of T while (i < S.Length || j < T.Length) { if (S[i] == T[i]) { i++; j++; } else { j = next[j];//当T[i]!=S[i]时,不再直接从T[0]处开始比较了 } } if (j == T.Length) { return i – j; } return -1; }

 

注:KMP算法同样适应于其它类型的数组,不仅仅是用作字符串匹配,但在实际应用中,字符串匹配用的最多,不过幸运的是,C#的string类中有现成的IndexOf方法,而且还支持正则表达式。

 

    原文作者:查找算法
    原文地址: https://blog.csdn.net/yyhian/article/details/6552808
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞