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

来源:互联网 发布:通信系统仿真软件 编辑:程序博客网 时间:2024/06/05 04:54

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。

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

 

方法二:

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

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

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

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

 

 

      还是上面的两个字符串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函数代码

KMP代码示例

 

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

 

原创粉丝点击