• KMP算法——字符匹配


     暴力匹配:

        假设现在我们面临这样一个问题:有一个文本串S,和一个模式串P,现在要查找P在S中的位置,怎么查找呢? 

        如果用暴力匹配的思路,并假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置,则有: 

          如果当前字符匹配成功(即S[i] == P[j]),则i++,j++,继续匹配下一个字符;

         如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0。

          理清楚了暴力匹配算法的流程及内在的逻辑,咱们可以写出暴力匹配的代码,如下:

        

     1 int ViolentMatch(char* s, char* p)
     2 
     3 {
     4 
     5     int sLen = strlen(s);
     6 
     7     int pLen = strlen(p);
     8 
     9  
    10 
    11     int i = 0;
    12 
    13     int j = 0;
    14 
    15     while (i < sLen && j < pLen)
    16 
    17     {
    18 
    19         if (s[i] == p[j])
    20 
    21         {
    22 
    23             //①如果当前字符匹配成功(即S[i] == P[j]),则i++,j++    
    24 
    25             i++;
    26 
    27             j++;
    28 
    29         }
    30 
    31         else
    32 
    33         {
    34 
    35             //②如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0    
    36 
    37             i = i - j + 1;  //i回到前一次遍历对比的位置
    38 
    39             j = 0;
    40 
    41         }
    42 
    43     }
    44 
    45     //匹配成功,返回模式串p在文本串s中的位置,否则返回-1
    46 
    47     if (j == pLen)
    48 
    49         return i - j;
    50 
    51     else
    52 
    53         return -1;
    54 
    55 }

        举个例子,如果给定文本串S“BBC ABCDAB ABCDABCDABDE”,和模式串P“ABCDABD”,现在要拿模式串P去跟文本串S匹配,整个过程如下所示:

        1. S[0]为B,P[0]为A,不匹配,执行第②条指令:“如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0”,S[1]跟P[0]匹配,相当于文本串要往右移动一位(i=1,j=0) 

        2. S[1]跟P[0]还是不匹配,继续执行第②条指令:“如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0”,S[2]跟P[0]匹配(i=2,j=0),从而文本串串不断的向右移动一位(不断的执行“令i = i - (j - 1),j = 0”,i从2变到4,j一直为0 

        3. 直到S[4]跟P[0]匹配成功(i=4,j=0),此时按照上面的暴力匹配算法的思路,转而执行第①条指令:“如果当前字符匹配成功(即S[i] == P[j]),则i++,j++”,可得S[i]为S[5],P[j]为P[1],即接下来S[5]跟P[1]匹配(i=5,j=1)  

        4. S[5]跟P[1]匹配成功,继续执行第①条指令:“如果当前字符匹配成功(即S[i] == P[j]),则i++,j++”,得到S[6]跟P[2]匹配(i=6,j=2),如此进行下去 

        5. 直到S[10]为空格字符,P[6]为字符D(i=10,j=6),因为不匹配,重新执行第②条指令:“如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0”,相当于S[5]跟P[0]匹配(i=5,j=0)  

        6. 至此,我们可以看到,如果按照暴力匹配算法的思路,尽管之前文本串和模式串已经分别匹配到了S[9]、P[5],但因为S[10]跟P[6]不匹配,所以文本串回溯到S[5],模式串回溯到P[0],从而让S[5]跟P[0]匹配。 

        而S[5]肯定跟P[0]失配。为什么呢?因为在之前第4步匹配中,我们已经得知S[5] = P[1] = B,而P[0] = A,即P[1] != P[0],故S[5]必定不等于P[0],所以回溯过去必然会导致失配。那有没有一种算法,让i 不往回退,只需要移动j 即可呢? 

        答案是肯定的。这种算法就是本文的主旨KMP算法,它利用之前已经部分匹配这个有效信息,保持i 不回溯,通过修改j 的位置,让模式串尽量地移动到有效的位置。

       

     KMP算法

     定义

        Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”,常用于在一个文本串S内查找一个模式串P 的出现位置,这个算法由Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年联合发表,故取这3人的姓氏命名此算法。 

        下面先直接给出KMP的算法流程(如果感到一点点不适,没关系,坚持下,稍后会有具体步骤及解释,越往后看越会柳暗花明☺): 

      假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置

      如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++,继续匹配下一个字符;

      如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]。此举意味着失配时,模式串P相对于文本串S向右移动了j - next [j] 位。换言之,当匹配失败时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值(next 数组的求解会在下文的3.3.3节中详细阐述),即移动的实际位数为:j - next[j],且此值大于等于1。 

      很快,你也会意识到next 数组各值的含义:代表当前字符之前的字符串中,有多大长度的相同前缀后缀

      例如如果next [j] = k,代表j 之前的字符串中有最大长度为k 的相同前缀后缀。

      此也意味着在某个字符失配时,该字符对应的next 值会告诉你下一步匹配中,模式串应该跳到哪个位置(跳到next [j] 的位置)。如果next [j] 等于0或-1,则跳到模式串的开头字符,若next [j] = k 且 k > 0,代表下次匹配跳到j 之前的某个字符,而不是跳到开头,且具体跳过了k 个字符。

      再强调一遍,next[i]就是子串s[0i]的最长相等前后缀的前缀最后一位的下标

      next数组的含义就是当j+1位失配时,j应该回退到的位置。

       

       由此可以总结出KMP算法的一般思路:

        ①初始化j=-1,表示pattern当前已被匹配的最后位。

        ②让i遍历文本串text,对每个i,执行③④来试图匹配text[i]pattern[i+1]

        ③不断令j=next[i],直到j回退为-1,或是text[i]=pattern[i+1]成立。

        ④如果text[i]=pattern[i+1],则j++。如果j达到m-1,说明pattern是text的子串,返回true。

    KMP算法的代码如下:

        转换成代码表示,则是:

        

     1 //next的数组的原理就是复制一次原数组,然后和原数组向后差一位进行匹配
     2 void getNext(vector<int>&next)
     3 {
     4     int j = 0;
     5     int k = -1;//k是关键,k=0就是求前缀数组,k=-1就是将前缀数组向后移一位
     6     int len = pattern.length();
     7     next[0] = -1;
     8     while (j < len-1)
     9     {
    10         if (k == -1 || pattern[j] == pattern[k])//将复制数组与原数组匹配
    11         {
    12             ++k;
    13             ++j;
    14             next[j] = k;
    15         }
    16         else
    17             k = next[k];//又从复制数组的头开始匹配,-1的位置
    18     }
    19 }
    20 int KMP()
    21 {
    22     int i = 0, j = 0;
    23     int slen = text.length(), plen = pattern.length();
    24     vector<int>next(pattern.length());
    25     getNext(next);
    26     while (i < slen && j < plen)
    27     {
    28         if (j == -1 || text[i] == pattern[j])//j==-1表示第一个字母都没有匹配上
    29         {
    30             ++i;
    31             ++j;
    32         }
    33         else
    34             j = next[j];//一旦没匹配,就跳next中的步数
    35     }
    36     if (j == plen)//匹配成功
    37         return i - j;
    38     else
    39         return -1;
    40 }

    详细步骤讲解:

    ①寻找前缀后缀最长公共元素长度

      对于P = p0 p1 ...pj-1 pj,寻找模式串P中长度最大且相等的前缀和后缀。如果存在p0 p1 ...pk-1 pk = pj- k pj-k+1...pj-1 pj,那么在包含pj的模式串中有最大长度为k+1的相同前缀后缀。举个例子,如果给定的模式串为“abab”,那么它的各个子串的前缀后缀的公共元素的最大长度如下表格所示:

       

      比如对于字符串aba来说,它有长度为1的相同前缀后缀a;而对于字符串abab来说,它有长度为2的相同前缀后缀ab(相同前缀后缀的长度为k + 1,k + 1 = 2)。

     

    ②求next数组

      next 数组考虑的是除当前字符外的最长相同前缀后缀,所以通过第①步骤求得各个前缀后缀的公共元素的最大长度后,只要稍作变形即可:将第①步骤中求得的值整体右移一位,然后初值赋为-1,如下表格所示:

       

      比如对于aba来说,第3个字符a之前的字符串ab中有长度为0的相同前缀后缀,所以第3个字符a对应的next值为0;而对于abab来说,第4个字符b之前的字符串aba中有长度为1的相同前缀后缀a,所以第4个字符b对应的next值为1(相同前缀后缀的长度为k,k = 1)。

     

    ③根据next数组进行匹配

      匹配失配,j = next [j],模式串向右移动的位数为:j - next[j]。换言之,当模式串的后缀pj-k pj-k+1, ..., pj-1 跟文本串si-k si-k+1, ..., si-1匹配成功,但pj 跟si匹配失败时,因为next[j] = k,相当于在不包含pj的模式串中有最大长度为k 的相同前缀后缀,即p0 p1 ...pk-1 = pj-k pj-k+1...pj-1,故令j = next[j],从而让模式串右移j - next[j] 位,使得模式串的前缀p0 p1, ..., pk-1对应着文本串 si-k si-k+1, ..., si-1,而后让pk 跟si 继续匹配。如下图所示:

       

       综上,KMP的next 数组相当于告诉我们:当模式串中的某个字符跟文本串中的某个字符匹配失配时,模式串下一步应该跳到哪个位置。如模式串中在j 处的字符跟文本串在i 处的字符匹配失配时,下一步用next [j] 处的字符继续跟文本串i 处的字符匹配,相当于模式串向右移动 j - next[j] 位。

         接下来,分别具体解释上述3个步骤。 

     

    解释:

    1、寻找最长前缀后缀 

          如果给定的模式串是:“ABCDABD”,从左至右遍历整个模式串,其各个子串的前缀后缀分别如下表格所示:

       

       也就是说,原模式串子串对应的各个前缀后缀的公共元素的最大长度表为(下简称《最大长度表》):

       

     2、基于《最大长度表》匹配

      最长前缀和最长后缀的匹配长度:

        即PreIndex = 0,  LastIndex = N;

        前缀指针向后移动,后缀指针向前移动,前缀指针不能到最后的一个字符,后缀指针不能到第一个字符,然后查找他们最长【从左至右的顺序】的相同长度。

        上图所示:str = "abcabcd"

        一般最长前缀和最长后缀值为: 

          index[0] = -1, index[1] == 0, 因为0位无前字符,1位也无不满足后缀指针不等于第一个字符。

        其他的需要计算:

          d的标记为3:即以0位置的a为前缀指针,d前面的字母c为后缀指针,找到的相同最长字符为"abc"  == 3

     

      因为模式串中首尾可能会有重复的字符,故可得出下述结论:

        失配时,模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应的最大长度值

     

      下面,咱们就结合之前的《最大长度表》和上述结论,进行字符串的匹配。

        如果给定文本串“BBC ABCDAB ABCDABCDABDE”,和模式串“ABCDABD”,现在要拿模式串去跟文本串匹配,如下图所示:

         

       1). 因为模式串中的字符A跟文本串中的字符B、B、C、空格一开始就不匹配,所以不必考虑结论,直接将模式串不断的右移一位即可,直到模式串中的字符A跟文本串的第5个字符A匹配成功:

       

       2). 继续往后匹配,当模式串最后一个字符D跟文本串匹配时失配,显而易见,模式串需要向右移动。但向右移动多少位呢?因为此时已经匹配的字符数为6个(ABCDAB),然后根据《最大长度表》可得失配字符D的上一位字符B对应的长度值为2,所以根据之前的结论,可知需要向右移动6 - 2 = 4 位。

       

       3). 模式串向右移动4位后,发现C处再度失配,因为此时已经匹配了2个字符(AB),且上一位字符B对应的最大长度值为0,所以向右移动:2 - 0 =2 位。

       

       4). A与空格失配,向右移动1 位。

       

       5). 继续比较,发现D与C 失配,故向右移动的位数为:已匹配的字符数6减去上一位字符B对应的最大长度2,即向右移动6 - 2 = 4 位。

       

       6). 经历第5步后,发现匹配成功,过程结束。

       

       通过上述匹配过程可以看出,问题的关键就是寻找模式串中最大长度的相同前缀和后缀,找到了模式串中每个字符之前的前缀和后缀公共部分的最大长度后,便可基于此匹配。而这个最大长度便正是next 数组要表达的含义。

     

    3、根据《最大长度表》求next 数组

        由上文,我们已经知道,字符串“ABCDABD”各个前缀后缀的最大公共元素长度分别为:

       

       而且,根据这个表可以得出下述结论:

        失配时,模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应的最大长度值

      上文利用这个表和结论进行匹配时,我们发现,当匹配到一个字符失配时,其实没必要考虑当前失配的字符,更何况我们每次失配时,都是看的失配字符的上一位字符对应的最大长度值。如此,便引出了next 数组。 

        给定字符串“ABCDABD”,可求得它的next 数组如下:

       

       把next 数组跟之前求得的最大长度表对比后,不难发现,next 数组相当于“最大长度值” 整体向右移动一位,然后初始值赋为-1。意识到了这一点,你会惊呼原来next 数组的求解竟然如此简单:就是找最大对称长度的前缀后缀,然后整体右移一位,初值赋为-1(当然,你也可以直接计算某个字符对应的next值,就是看这个字符之前的字符串中有多大长度的相同前缀后缀)。

          换言之,对于给定的模式串:ABCDABD,它的最大长度表及next 数组分别如下:

       

      根据最大长度表求出了next 数组后,从而有失配时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值

      而后,你会发现,无论是基于《最大长度表》的匹配,还是基于next 数组的匹配,两者得出来的向右移动的位数是一样的。为什么呢?因为:

        根据《最大长度表》,失配时,模式串向右移动的位数 = 已经匹配的字符数 - 失配字符的上一位字符的最大长度值

        而根据《next 数组》,失配时,模式串向右移动的位数 = 失配字符的位置 - 失配字符对应的next

       其中,从0开始计数时,失配字符的位置 = 已经匹配的字符数(失配字符不计数)

      而失配字符对应的next  = 失配字符的上一位字符的最大长度值,两相比较,结果必然完全一致。

      所以,你可以把《最大长度表》看做是next 数组的雏形,甚至就把它当做next 数组也是可以的,区别不过是怎么用的问题。

     

    4、通过代码递推计算next 数组

        接下来,咱们来写代码求下next 数组。

        基于之前的理解,可知计算next 数组的方法可以采用递推:

      1). 如果对于值k,已有p0 p1, ..., pk-1 = pj-k pj-k+1, ..., pj-1,相当于next[j] = k。

        此意味着什么呢?究其本质,next[j] = k 代表p[j] 之前的模式串子串中,有长度为k 的相同前缀和后缀。有了这个next 数组,在KMP匹配中,当模式串中j 处的字符失配时,下一步用next[j]处的字符继续跟文本串匹配,相当于模式串向右移动j - next[j] 位。

     

      2). 下面的问题是:已知next [0, ..., j],如何求出next [j + 1]呢?

        对于P的前j+1个序列字符:

        若p[k] == p[j],则next[j + 1 ] = next [j] + 1 = k + 1

        若p[k ] ≠ p[j],如果此时p[ next[k] ] == p[j ],则next[ j + 1 ] =  next[k] + 1否则继续递归前缀索引k = next[k],而后重复此过程。 相当于在字符p[j+1]之前不存在长度为k+1的前缀"p0 p1, …, pk-1 pk"跟后缀“pj-k pj-k+1, …, pj-1 pj"相等,那么是否可能存在另一个值t+1 < k+1,使得长度更小的前缀 “p0 p1, …, pt-1 pt” 等于长度更小的后缀 “pj-t pj-t+1, …, pj-1 pj” 呢?如果存在,那么这个t+1 便是next[ j+1]的值,此相当于利用已经求得的next 数组(next [0, ..., k, ..., j])进行P串前缀跟P串后缀的匹配。

        如下图所示,假定给定模式串ABCDABCE,且已知next [j] = k(相当于“p0 pk-1” = “pj-k pj-1” = AB,可以看出k为2),现要求next [j + 1]等于多少?因为pk = pj = C,所以next[j + 1] = next[j] + 1 = k + 1(可以看出next[j + 1] = 3)。代表字符E前的模式串中,有长度k+1 的相同前缀后缀。

         

        但如果pk != pj 呢?说明“p0 pk-1 pk”  ≠ “pj-k pj-1 pj”。换言之,当pk != pj后,字符E前有多大长度的相同前缀后缀呢?很明显,因为C不同于D,所以ABC 跟 ABD不相同,即字符E前的模式串没有长度为k+1的相同前缀后缀,也就不能再简单的令:next[j + 1] = next[j] + 1 。所以,咱们只能去寻找长度更短一点的相同前缀后缀。

        

         结合上图来讲,若能在前缀“ p0 pk-1 pk ” 中不断的递归前缀索引k = next [k],找到一个字符pk’ 也为D,代表pk’ = pj,且满足p0 pk'-1 pk' = pj-k' pj-1 pj,则最大相同的前缀后缀长度为k' + 1,从而next [j + 1] = k’ + 1 = next [k' ] + 1。否则前缀中没有D,则代表没有相同的前缀后缀,next [j + 1] = 0。

     

      那为何递归前缀索引k = next[k],就能找到长度更短的相同前缀后缀呢?这又归根到next数组的含义。我们拿前缀 p0 pk-1 pk 去跟后缀pj-k pj-1 pj匹配,如果pk 跟pj 失配,下一步就是用p[next[k]] 去跟pj 继续匹配,如果p[ next[k] ]跟pj还是不匹配,则需要寻找长度更短的相同前缀后缀,即下一步用p[ next[ next[k] ] ]去跟pj匹配。此过程相当于模式串的自我匹配,所以不断的递归k = next[k],直到要么找到长度更短的相同前缀后缀,要么没有长度更短的相同前缀后缀。如下图所示:   

       

       找到p[k]对应的next[k],根据对称性,只需再判断p[next[k]]与p[j]是否相等即可,于是令k = next[k],这里恰好就使用了递归的思路。其实我觉得不要一开始就陷入递归的方法中,换一种思路,直接从考虑对称性入手,可直接得出k = next[k],而这正好是递归罢了。

      

      所以,因最终在前缀ABC中没有找到D,故E的next 值为0:

        模式串的后缀:ABDE

        模式串的前缀:ABC

        前缀右移两位:     ABC

      那能否举一个能在前缀中找到字符D的例子呢?OK,咱们便来看一个能在前缀中找到字符D的例子,如下图所示:

      

       给定模式串DABCDABDE,我们很顺利的求得字符D之前的“DABCDAB”的各个子串的最长相同前缀后缀的长度分别为0 0 0 0 1 2 3,但当遍历到字符D,要求包括D在内的“DABCDABD”最长相同前缀后缀时,我们发现pj处的字符D跟pk处的字符C不一样,换言之,前缀DABC的最后一个字符C 跟后缀DABD的最后一个字符D不相同,所以不存在长度为4的相同前缀后缀。

       怎么办呢?既然没有长度为4的相同前缀后缀,咱们可以寻找长度短点的相同前缀后缀,最终,因在p0处发现也有个字符D,p0 = pj,所以p[j]对应的长度值为1,相当于E对应的next 值为1(即字符E之前的字符串“DABCDABD”中有长度为1的相同前缀和后缀)。

     

       综上,可以通过递推求得next 数组,代码如下所示:

     1 //next的数组的原理就是复制一次原数组,然后和原数组向后差一位进行匹配
     2 void getNext(vector<int>&next)
     3 {
     4     int j = 0;
     5     int k = -1;//k是关键,k=0就是求前缀数组,k=-1就是将前缀数组向后移一位
     6     int len = pattern.length();
     7     next[0] = -1;
     8     while (j < len-1)
     9     {
    10         if (k == -1 || pattern[j] == pattern[k])//将复制数组与原数组匹配
    11         {
    12             ++k;
    13             ++j;
    14             next[j] = k;
    15         }
    16         else
    17             k = next[k];//又从复制数组的头开始匹配,-1的位置
    18     }
    19 }

     扩展:BM算法

        KMP的匹配是从模式串的开头开始匹配的,而1977年,德克萨斯大学的Robert S. Boyer教授和J Strother Moore教授发明了一种新的字符串匹配算法:Boyer-Moore算法,简称BM算法。该算法从模式串的尾部开始匹配,且拥有在最坏情况下O(N)的时间复杂度。在实践中,比KMP算法的实际效能高。

       BM算法定义了两个规则:

        坏字符规则:

          当文本串中的某个字符跟模式串的某个字符不匹配时,我们称文本串中的这个失配字符为坏字符,此时模式串需要向右移动,移动的位数 = 坏字符在模式串中的位置 - 坏字符在模式串中最右出现的位置。此外,如果"坏字符"不包含在模式串之中,则最右出现位置为-1。

        好后缀规则:

          当字符失配时,后移位数 = 好后缀在模式串中的位置 - 好后缀在模式串上一次出现的位置,且如果好后缀在模式串中没有再次出现,则为-1。

       下面举例说明BM算法。例如,给定文本串“HERE IS A SIMPLE EXAMPLE”,和模式串“EXAMPLE”,现要查找模式串是否在文本串中,如果存在,返回模式串在文本串中的位置。

      1、 首先,"文本串"与"模式串"头部对齐,从尾部开始比较。"S"与"E"不匹配。这时,"S"就被称为"坏字符"(bad character),即不匹配的字符,它对应着模式串的第6位。且"S"不包含在模式串"EXAMPLE"之中(相当于最右出现位置是-1),这意味着可以把模式串后移6-(-1)=7位,从而直接移到"S"的后一位。

       

        2、依然从尾部开始比较,发现"P"与"E"不匹配,所以"P"是"坏字符"。但是,"P"包含在模式串 "EXAMPLE"之中。因为“P”这个“坏字符”对应着模式串的第6位(从0开始编号),且在模式串中的最右出现位置为4,所以,将模式串后移6-4=2位,两个"P"对齐。

       

       3、依次比较,得到 “MPLE”匹配,称为"好后缀"(good suffix),即所有尾部匹配的字符串。注意,"MPLE"、"PLE"、"LE"、"E"都是好后缀。

       

       4、发现“I”与“A”不匹配:“I”是坏字符。如果是根据坏字符规则,此时模式串应该后移2-(-1)=3位。问题是,有没有更优的移法?

       

        5、更优的移法是利用好后缀规则:当字符失配时,后移位数 = 好后缀在模式串中的位置 - 好后缀在模式串中上一次出现的位置,且如果好后缀在模式串中没有再次出现,则为-1。

        所有的“好后缀”(MPLE、PLE、LE、E)之中,只有“E”在“EXAMPLE”的头部出现,所以后移6-0=6位。

        可以看出,“坏字符规则”只能移3位,“好后缀规则”可以移6位。每次后移这两个规则之中的较大值。这两个规则的移动位数,只与模式串有关,与原文本串无关。

       

       6、继续从尾部开始比较,“P”与“E”不匹配,因此“P”是“坏字符”,根据“坏字符规则”,后移 6 - 4 = 2位。因为是最后一位就失配,尚未获得好后缀。

       

       由上可知,BM算法不仅效率高,而且构思巧妙,容易理解。

     1 //bad character数组,128   ASCll码为数组大小
     2 vector<int> preBmBc(string ps) {
     3     vector<int> BC(128, ps.size());
     4     for (size_t i = 0; i < ps.size(); i++)
     5         BC[ps[i]] = ps.size() - i - 1;//记住每个字母的最右端的位置,重复字母中,后端会把前端覆盖掉的
     6     //记住,这位置的反着的,即ps.size()-i;
     7 
     8 return BC;
     9 }
    10 void BM(string text, string pattern)
    11 {
    12     vector<int> matched;//匹配的好字符
    13     vector<int> BC = preBmBc(pattern);//创建bad character数组
    14 
    15 int tlen = text.size();
    16     int plen = pattern.size();
    17 
    18 int tindex = 0;//text索引
    19     while (tindex + plen <= tlen) {
    20         int badmove = 0;//坏字符位置
    21         int goodmove = 0;//好字符位置
    22         for (size_t j = plen; j >= 0; j--)
    23         {
    24             if (text[tindex + j - 1] != pattern[j - 1]) {//匹配失败
    25                 badmove = BC[text[tindex + j - 1]]; //bad character移动步数,不存在则是将整个模式字符后移
    26                 break;
    27             }        
    28 
    29            if (j == 0) {//匹配到
    30 
    31                 return;
    32             }
    33         }
    34         tindex += badmove;
    35     }
    36 }

     扩展:Sunday算法

        上文中,我们已经介绍了KMP算法和BM算法,这两个算法在最坏情况下均具有线性的查找时间。但实际上,KMP算法并不比最简单的c库函数strstr()快多少,而BM算法虽然通常比KMP算法快,但BM算法也还不是现有字符串查找算法中最快的算法,本文最后再介绍一种比BM算法更快的查找算法即Sunday算法。

         Sunday算法由Daniel M.Sunday在1990年提出,它的思想跟BM算法很相似:

        只不过Sunday算法是从前往后匹配,在匹配失败时关注的是文本串中参加匹配的最末位字符的下一位字符

        如果该字符没有在模式串中出现则直接跳过,即移动位数 = 匹配串长度 + 1

        否则,其移动位数 = 模式串中最右端的该字符到末尾的距离+1

       

      下面举个例子说明下Sunday算法。假定现在要在文本串"substring searching algorithm"中查找模式串"search"。

      1、刚开始时,把模式串与文本串左边对齐:

       

        2、结果发现在第2个字符处发现不匹配,不匹配时关注文本串中参加匹配的最末位字符的下一位字符,即标粗的字符 i,因为模式串search中并不存在i,所以模式串直接跳过一大片,向右移动位数 = 匹配串长度 + 1 = 6 + 1 = 7,从 i 之后的那个字符(即字符n)开始下一步的匹配,如下图:

       

        3 结果第一个字符就不匹配,再看文本串中参加匹配的最末位字符的下一位字符,是'r',它出现在模式串中的倒数第3位,于是把模式串向右移动3(r 到模式串末尾的距离 + 1 = 2 + 1 =3),使两个'r'对齐,如下: 

      

      4、匹配成功。

        回顾整个过程,我们只移动了两次模式串就找到了匹配位置,缘于Sunday算法每一步的移动量都比较大,效率很高。完。

       

     1 int Sunday(string text, string pattern)
     2 {
     3     int i = 0;
     4     int j = 0;
     5     while (i < text.length() && j < pattern.length())
     6     {
     7         if (text[i] == pattern[j])
     8         {
     9             i++;
    10             j++;
    11         }
    12         else
    13         {
    14             int k = pattern.length() - 1;
    15 
    16 while (k >= 0)
    17             {
    18                 if (text[i + pattern.length() + 1] == pattern[k])//查询后面的哪个字母在pattern是否存在
    19                 {
    20                     break;
    21                 }
    22                 else
    23                 {
    24                     k--;
    25                 }
    26             }
    27             i += (pattern.length() - (k + 1) + 1);
    28             j = 0;
    29         }
    30     }
    31     if (j == pattern.length())//匹配成功
    32         return i;
    33     else
    34         return -1;
    35 }
  • 相关阅读:
    心得
    构建之法--界面化的简单四则运算
    构建之法--简单四则运算
    构建之法--初识Git
    构建之法---第一次作业
    【软件工程实践】结对项目-四则运算 “软件”之升级版
    第三次作业:个人项目-小学四则运算 “软件”之初版
    分布式版本控制系统Git的安装与使用
    第一次作业-准备
    字符串、文件操作,英文词率统计预处理
  • 原文地址:https://www.cnblogs.com/zzw1024/p/11945577.html
Copyright © 2020-2023  润新知