彻头彻尾地理解KMP字符串模式匹配算法

1. 引例-暴力匹配算法

    假设如今咱们面临这样一个问题:有一个文本串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。spa

    理清楚了暴力匹配算法的流程及内在的逻辑,我们能够写出暴力匹配的代码,以下:.net

int ViolentMatch(char* s, char* p)  
{  
    int sLen = strlen(s);  
    int pLen = strlen(p);  
  
    int i = 0;  
    int j = 0;  
    while (i < sLen && j < pLen)  
    {  
        if (s[i] == p[j])  
        {  
            //①若是当前字符匹配成功(即S[i] == P[j]),则i++,j++      
            i++;  
            j++;  
        }  
        else  
        {  
            //②若是失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0      
            i = i - j + 1;  
            j = 0;  
        }  
    }  
    //匹配成功,返回模式串p在文本串s中的位置,不然返回-1  
    if (j == pLen)  
        return i - j;  
    else  
        return -1;  
}

    举个例子,若是给定文本串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)code

    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)orm

    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)blog

     

    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 的位置,让模式串尽可能地移动到有效的位置。

2. KMP算法

2.1 定义

    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 个字符。    转换成代码表示,则是:

int KmpSearch(char* s, char* p)  
{  
    int i = 0;  
    int j = 0;  
    int sLen = strlen(s);  
    int pLen = strlen(p);  
    while (i < sLen && j < pLen)  
    {  
        //①若是j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++      
        if (j == -1 || s[i] == p[j])  
        {  
            i++;  
            j++;  
        }  
        else  
        {  
            //②若是j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]      
            //next[j]即为j所对应的next值        
            j = next[j];  
        }  
    }  
    if (j == pLen)  
        return i - j;  
    else  
        return -1;  
}

    继续拿以前的例子来讲,当S[10]跟P[6]匹配失败时,KMP不是跟暴力匹配那样简单的把模式串右移一位,而是执行第②条指令:“若是j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]”,即j 从6变到2(后面咱们将求得P[6],即字符D对应的next 值为2),因此至关于模式串向右移动的位数为j - next[j](j - next[j] = 6-2 = 4)。

    向右移动4位后,S[10]跟P[2]继续匹配。为何要向右移动4位呢,由于移动4位后,模式串中又有个“AB”能够继续跟S[8]S[9]对应着,从而不用让i 回溯。至关于在除去字符D的模式串子串中寻找相同的前缀和后缀,而后根据前缀后缀求出next 数组,最后基于next 数组进行匹配(不关心next 数组是怎么求来的,只想看匹配过程是咋样的,可直接跳到下文3.3.4节)。

2.2 步骤

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

    • 对于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个步骤。

2.3 解释

2.3.1 寻找最长前缀后缀

    若是给定的模式串是:“ABCDABD”,从左至右遍历整个模式串,其各个子串的前缀后缀分别以下表格所示:    也就是说,原模式串子串对应的各个前缀后缀的公共元素的最大长度表为(下简称《最大长度表》):


2.3.2 基于《最大长度表》匹配

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

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

    下面,我们就结合以前的《最大长度表》和上述结论,进行字符串的匹配。若是给定文本串“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 数组要表达的含义。

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

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

    并且,根据这个表能够得出下述结论

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

    上文利用这个表和结论进行匹配时,咱们发现,当匹配到一个字符失配时,其实不必考虑当前失配的字符,更况且咱们每次失配时,都是看的失配字符的上一位字符对应的最大长度值。如此,便引出了next 数组。    给定字符串“ABCDABD”,可求得它的next 数组以下:

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

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


    根据最大长度表求出了next 数组后,从而有

失配时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值

    然后,你会发现,不管是基于《最大长度表》的匹配,仍是基于next 数组的匹配,二者得出来的向右移动的位数是同样的。为何呢?由于:

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

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

    • 其中,从0开始计数时,失配字符的位置 = 已经匹配的字符数(失配字符不计数),而失配字符对应的next 值 = 失配字符的上一位字符的最大长度值,两相比较,结果必然彻底一致。

    因此,你能够把《最大长度表》看作是next 数组的雏形,甚至就把它当作next 数组也是能够的,区别不过是怎么用的问题。

2.3.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] 位。

举个例子,以下图,根据模式串“ABCDABD”的next 数组可知失配位置的字符D对应的next 值为2,表明字符D前有长度为2的相同前缀和后缀(这个相同的前缀后缀即为“AB”),失配后,模式串须要向右移动j - next [j] = 6 - 2 =4位。

向右移动4位后,模式串中的字符C继续跟文本串匹配。

 
  • 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串后缀的匹配。

   通常的文章或教材可能就此一笔带过,但大部分的初学者可能仍是不能很好的理解上述求解next 数组的原理,故接下来,我再来着重说明下。    以下图所示,假定给定模式串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],直到要么找到长度更小的相同前缀后缀,要么没有长度更小的相同前缀后缀。    因此,因最终在前缀ABC中没有找到D,故E的next 值为0:

模式串的后缀:AB     DE
模式串的前缀:AB     C
前缀右移两位:          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。    综上,能够经过递推求得next 数组,代码以下所示:

void GetNext(char* p,int next[])  
{  
    int pLen = strlen(p);  
    next[0] = -1;  
    int k = -1;  
    int j = 0;  
    while (j < pLen - 1)  
    {  
        //p[k]表示前缀,p[j]表示后缀  
        if (k == -1 || p[j] == p[k])   
        {  
            ++k;  
            ++j;  
            next[j] = k;  
        }  
        else   
        {  
            k = next[k];  
        }  
    }  
}

    用代码从新计算下“ABCDABD”的next 数组,以验证以前经过“最长相同前缀后缀长度值右移一位,而后初值赋为-1”获得的next 数组是否正确,计算结果以下表格所示:


    从上述表格能够看出,不管是以前经过“最长相同前缀后缀长度值右移一位,而后初值赋为-1”获得的next 数组,仍是以后经过代码递推计算求得的next 数组,结果是彻底一致的。

2.3.5 基于《next 数组》匹配

    下面,咱们来基于next 数组进行匹配。

    仍是给定文本串“BBC ABCDAB ABCDABCDABDE”,和模式串“ABCDABD”,如今要拿模式串去跟文本串匹配,以下图所示:

    在正式匹配以前,让咱们来再次回顾下上文2.1节所述的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 值,即移动的实际位数为:j - next[j],且此值大于等于1。

  • 1. 最开始匹配时

    • P[0]跟S[0]匹配失败

      • 因此执行“若是j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]”,因此j = -1,故转而执行“若是j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++”,获得i = 1,j = 0,即P[0]继续跟S[1]匹配。

    • P[0]跟S[1]又失配,j再次等于-1,i、j继续自增,从而P[0]跟S[2]匹配。

    • P[0]跟S[2]失配后,P[0]又跟S[3]匹配。

    • P[0]跟S[3]再失配,直到P[0]跟S[4]匹配成功,开始执行此条指令的后半段:“若是j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++”。

  • 2. P[1]跟S[5]匹配成功,P[2]跟S[6]也匹配成功, ...,直到当匹配到P[6]处的字符D时失配(即S[10] != P[6]),因为P[6]处的D对应的next 值为2,因此下一步用P[2]处的字符C继续跟S[10]匹配,至关于向右移动:j - next[j] = 6 - 2 =4 位。

  • 3. 向右移动4位后,P[2]处的C再次失配,因为C对应的next值为0,因此下一步用P[0]处的字符继续跟S[10]匹配,至关于向右移动:j - next[j] = 2 - 0 = 2 位。

  • 4. 移动两位以后,A 跟空格不匹配,模式串后移1 位。

  • 5. P[6]处的D再次失配,由于P[6]对应的next值为2,故下一步用P[2]继续跟文本串匹配,至关于模式串向右移动 j - next[j] = 6 - 2 = 4 位。

  • 6. 匹配成功,过程结束。

    匹配过程如出一辙。也从侧面佐证了,next 数组确实是只要将各个最大前缀后缀的公共元素的长度值右移一位,且把初值赋为-1 便可。

2.3.6 基于《最大长度表》与基于《next 数组》等价

    咱们已经知道,利用next 数组进行匹配失配时,模式串向右移动 j - next [ j ] 位,等价于已匹配字符数 - 失配字符的上一位字符所对应的最大长度值。缘由是:

  1. j 从0开始计数,那么当数到失配字符时,j 的数值就是已匹配的字符数;

  2. 因为next 数组是由最大长度值表总体向右移动一位(且初值赋为-1)获得的,那么失配字符的上一位字符所对应的最大长度值,即为当前失配字符的next 值。

    但为什么本文不直接利用next 数组进行匹配呢?由于next 数组很差求,而一个字符串的前缀后缀的公共元素的最大长度值很容易求。例如若给定模式串“ababa”,要你快速口算出其next 数组,乍一看,每次求对应字符的next值时,还得把该字符排除以外,而后看该字符以前的字符串中有最大长度为多大的相同前缀后缀,此过程不够直接。而若是让你求其前缀后缀公共元素的最大长度,则很容易直接得出结果:0 0 1 2 3,以下表格所示:

    而后这5个数字 所有总体右移一位,且初值赋为-1,即获得其next 数组:-1 0 0 1 2。

2.3.7 Next 数组的优化

   行文至此,我们全面了解了暴力匹配的思路、KMP算法的原理、流程、流程之间的内在逻辑联系,以及next 数组的简单求解(《最大长度表》总体右移一位,而后初值赋为-1)和代码求解,最后基于《next 数组》的匹配,看似洋洋洒洒,清晰透彻,但以上忽略了一个小问题。

    好比,若是用以前的next 数组方法求模式串“abab”的next 数组,可得其next 数组为-1 0 0 1(0 0 1 2总体右移一位,初值赋为-1),当它跟下图中的文本串去匹配的时候,发现b跟c失配,因而模式串右移j - next[j] = 3 - 1 =2位。

    右移2位后,b又跟c失配。事实上,由于在上一步的匹配中,已经得知p[3] = b,与s[3] = c失配,而右移两位以后,让p[ next[3] ] = p[1] = b 再跟s[3]匹配时,必然失配。问题出在哪呢?

    问题出在不应出现p[j] = p[ next[j] ]。为何呢?理由是:当p[j] != s[i] 时,下次匹配必然是p[ next [j]] 跟s[i]匹配,若是p[j] = p[ next[j] ],必然致使后一步匹配失败(由于p[j]已经跟s[i]失配,而后你还用跟p[j]等同的值p[next[j]]去跟s[i]匹配,很显然,必然失配),因此不能容许p[j] = p[ next[j ]]。若是出现了p[j] = p[ next[j] ]咋办呢?若是出现了,则须要再次递归,即令next[j] = next[ next[j] ]。

    因此,我们得修改下求next 数组的代码。[cpp] view plaincopyprint?在CODE上查看代码片派生到个人代码片

//优化事后的next 数组求法  
void GetNextval(char* p, int next[])  
{  
    int pLen = strlen(p);  
    next[0] = -1;  
    int k = -1;  
    int j = 0;  
    while (j < pLen - 1)  
    {  
        //p[k]表示前缀,p[j]表示后缀    
        if (k == -1 || p[j] == p[k])  
        {  
            ++j;  
            ++k;  
            //较以前next数组求法,改动在下面4行  
            if (p[j] != p[k])  
                next[j] = k;   //以前只有这一行  
            else  
                //由于不能出现p[j] = p[ next[j ]],因此当出现时须要继续递归,k = next[k] = next[next[k]]  
                next[j] = next[k];  
        }  
        else  
        {  
            k = next[k];  
        }  
    }  
}

    利用优化事后的next 数组求法,可知模式串“abab”的新next数组为:-1 0 -1 0。可能有些读者会问:原始next 数组是前缀后缀最长公共元素长度值右移一位, 而后初值赋为-1而得,那么优化后的next 数组如何快速心算出呢?实际上,只要求出了原始next 数组,即可以根据原始next 数组快速求出优化后的next 数组。仍是以abab为例,以下表格所示:

    

只要出现了p[next[j]] = p[j]的状况,则把next[j]的值再次递归。例如在求模式串“abab”的第2anext值时,若是是未优化的next值的话,第2a对应的next值为0,至关于第2a失配时,下一步匹配模式串会用p[0]处的a再次跟文本串匹配,必然失配。因此求第2anext值时,须要再次递归:next[2] = next[ next[2] ] = next[0] = -1此后,根据优化后的新next值可知2a失配时,执行“若是j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++j++,继续匹配下一个字符,同理,第2b对应的next值为0

对于优化后的next数组能够发现一点:若是模式串的后缀跟前缀相同,那么它们的next值也是相同的,例如模式串abcabc,它的前缀后缀都是abc,其优化后的next数组为:-1 0 0 -1 0 0,前缀后缀abcnext值都为-1 0 0

    而后引用下以前3.1节的KMP代码:

int KmpSearch(char* s, char* p)  
{  
    int i = 0;  
    int j = 0;  
    int sLen = strlen(s);  
    int pLen = strlen(p);  
    while (i < sLen && j < pLen)  
    {  
        //①若是j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++      
        if (j == -1 || s[i] == p[j])  
        {  
            i++;  
            j++;  
        }  
        else  
        {  
            //②若是j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]      
            //next[j]即为j所对应的next值        
            j = next[j];  
        }  
    }  
    if (j == pLen)  
        return i - j;  
    else  
        return -1;  
}

    接下来,我们继续拿以前的例子说明,整个匹配过程以下:

    1. S[3]与P[3]匹配失败。

    2. S[3]保持不变,P的下一个匹配位置是P[next[3]],而next[3]=0,因此P[next[3]]=P[0]与S[3]匹配。

    3.  因为上一步骤中P[0]与S[3]仍是不匹配。此时i=3,j=next [0]=-1,因为知足条件j==-1,因此执行“++i, ++j”,即主串指针下移一个位置,P[0]与S[4]开始匹配。最后j==pLen,跳出循环,输出结果i - j = 4(即模式串第一次在文本串中出现的位置),匹配成功,算法结束。

3.4 KMP的时间复杂度分析

    相信大部分读者读完上文以后,已经发觉其实理解KMP很是容易,无非是按部就班把握好下面几点:

  1. 若是模式串中存在相同前缀和后缀,即pj-k pj-k+1, ..., pj-1 = p0 p1, ..., pk-1,那么在pj跟si失配后,让模式串的前缀p0 p1...pk-1对应着文本串si-k si-k+1...si-1,然后让pk跟si继续匹配。

  2. 以前本应是pj跟si匹配,结果失配了,失配后,令pk跟si匹配,至关于j 变成了k,模式串向右移动j - k位。

  3. 由于k 的值是可变的,因此咱们用next[j]表示j处字符失配后,下一次匹配模式串应该跳到的位置。换言之,失配前是j,pj跟si失配时,用p[ next[j] ]继续跟si匹配,至关于j变成了next[j],因此,j = next[j],等价于把模式串向右移动j - next [j] 位。

  4. 而next[j]应该等于多少呢?next[j]的值由j 以前的模式串子串中有多大长度的相同前缀后缀所决定,若是j 以前的模式串子串中(不含j)有最大长度为k的相同前缀后缀,那么next [j] = k。

    如以前的图所示:


    接下来,我们来分析下KMP的时间复杂度。分析以前,先来回顾下KMP匹配算法的流程:

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] 位。

    咱们发现若是某个字符匹配成功,模式串首字符的位置保持不动,仅仅是i++、j++;若是匹配失配,i 不变(即 i 不回溯),模式串会跳过匹配过的next [j]个字符。整个算法最坏的状况是,当模式串首字符位于i - j的位置时才匹配成功,算法结束。    因此,若是文本串的长度为n,模式串的长度为m,那么匹配过程的时间复杂度为O(n),算上计算next的O(m)时间,KMP的总体时间复杂度为O(m + n)。

相关文章
相关标签/搜索