编程语言
首页 > 编程语言> > KMP算法的优化与详解

KMP算法的优化与详解

作者:互联网

1. KMP算法

1.1 定义

    Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”,常用于在一个文本串S内查找一个模式串P 的出现位置,这个算法由Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年联合发表,故取这3人的姓氏命名此算法。     下面先直接给出KMP的算法流程(如果感到一点点不适,没关系,坚持下,稍后会有具体步骤及解释:     很快,你也会意识到next 数组各值的含义:代表当前字符之前的字符串中,有多大长度的相同前缀后缀。例如如果next [j] = k,代表j 之前的字符串中有最大长度为 k 的相同前缀后缀。     此也意味着在某个字符失配时,该字符对应的next 值会告诉你下一步匹配中,模式串应该跳到哪个位置(跳到next [j] 的位置)。如果next [j] 等于0或-1,则跳到模式串的开头字符,若next [j] = k 且 k > 0,代表下次匹配跳到j 之前的某个字符,而不是跳到开头,且具体跳过了k 个字符。KMP代码如下:
 public static int getIndexOf(String s, String p) {
    if (s == null || p == null || p.length() < 1 || s.length() < p.length()) {
        return -1;
    }
    char[] arr_s = s.toCharArray();
    char[] arr_p = p.toCharArray();
    int i = 0;
    int j = 0;
    // 计算 模式串 的 next 数组,以供 KMP 比较时进行跳跃
    int[] next = getNextArray(arr_p);
</span><span style="color:#0000ff;">while</span> (i &lt; arr_s.length &amp;&amp; j &lt;<span style="color:#000000;"> arr_p.length) {
    </span><span style="color:#0000ff;">if</span> (arr_s[i] ==<span style="color:#000000;"> arr_p[j]) {
        </span><span style="color:#008000;">//</span><span style="color:#008000;"> 当两个字符相等时,两个指针同时向后移动</span>
        i++<span style="color:#000000;">;
        j</span>++<span style="color:#000000;">;
    } </span><span style="color:#0000ff;">else</span> <span style="color:#0000ff;">if</span> (next[j] == -1<span style="color:#000000;">) {
        </span><span style="color:#008000;">//</span><span style="color:#008000;"> 当 next[j]=-1 说明,j已经跳跃到模式串p的起始位置了,无法再次进行跳跃。
        </span><span style="color:#008000;">//</span><span style="color:#008000;"> 说明 arr_s[i] 与 模式串p 无法进行匹配,因此将 i 移动到下一个位置</span>
        i++<span style="color:#000000;">;
    } </span><span style="color:#0000ff;">else</span><span style="color:#000000;"> {
        </span><span style="color:#008000;">//</span><span style="color:#008000;"> 当两个字符不想等,则依据模式串p的 next[],我们可以将指针 j 向前跳跃到 next[j] 处,
        </span><span style="color:#008000;">//</span><span style="color:#008000;"> 相当于将 模式串 右移 j-next[j],然后继续将 arr_s[i] 与 arr_p[j] 进行匹配</span>
        j =<span style="color:#000000;"> next[j];
    }
}

</span><span style="color:#008000;">//</span><span style="color:#008000;"> 如果 j 能够成功遍历到 arr_p 的结束位置,说明能够在字符串 s 中匹配到模式串p.
</span><span style="color:#008000;">//</span><span style="color:#008000;"> 匹配的结束为止为 i,因此起始位置为 i-j. 否则说明无法匹配,返回-1.</span>
<span style="color:#0000ff;">return</span> j == arr_p.length ? i - j : -1<span style="color:#000000;">;

}

1.2 步骤

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

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

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

1.3 解释

1.3.1 寻找最长前缀后缀

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

 

1.3.2 基于《最大长度表》匹配(此部分为帮助大家理解next数组的由来而写,若大家能够理解next数组关注的为该字符前的字符串可跳过该部分)

     对最大长度表与next数组有困惑的可看该部分,否则推荐直接跳过该部分与最大长度表的相关知识。   因为模式串中首尾可能会有重复的字符,故可得出下述结论:
失配时,模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应的最大长度值

    下面,咱们就结合之前的《最大长度表》和上述结论,进行字符串的匹配。如果给定文本串“BBC ABCDAB ABCDABCDABDE”,和模式串“ABCDABD”,现在要拿模式串去跟文本串匹配,如下图所示:

        

           
           

          

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

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

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

 

 

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

 

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

 

 

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

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


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

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

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

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

 

1.3.4 通过代码递推计算next 数组(KMP算法的核心部分)

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

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

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

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

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

    如下图所示,假定给定模式串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。换言之,当p k != p j后,字符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(即字符E之前的字符串“DABCDABD”中有长度为1的相同前缀和后缀)。     综上,可以通过递推求得next 数组,代码如下所示:
// 计算 arr[] 的 next 数组
public static int[] getNextArray(char[] arr) {
    if (arr.length == 1) {
        return new int[]{-1};
    }
    int[] next = new int[arr.length];
</span><span style="color:#008000;">//</span><span style="color:#008000;"> 根据定义初始化next数组,0位置为-1,1位置为0.</span>
next[0] = -1<span style="color:#000000;">;
next[</span>1] = 0<span style="color:#000000;">;
</span><span style="color:#0000ff;">int</span> pos = 2;    <span style="color:#008000;">//</span><span style="color:#008000;"> 当前位置</span>
<span style="color:#0000ff;">int</span> cn = 0;     <span style="color:#008000;">//</span><span style="color:#008000;"> 当前位置前一个字符的 next[] 值(最长相等前后缀的长度)</span>
<span style="color:#0000ff;">while</span> (pos &lt;<span style="color:#000000;"> next.length) {
    </span><span style="color:#0000ff;">if</span> (arr[pos - 1] ==<span style="color:#000000;"> arr[cn]) {
        </span><span style="color:#008000;">//</span><span style="color:#008000;"> 当字符串的 pos-1 位置与 pos-1 位置字符所对应的最长相同前后缀的下一个字符 arr[next[pos-1]] 相等时
        </span><span style="color:#008000;">//</span><span style="color:#008000;"> 我们就能确定 next[pos] 的值为 pos-1 位置所对应的 next[pos-1] + 1,即 ++cn.</span>
        next[pos++] = ++<span style="color:#000000;">cn;
    } </span><span style="color:#0000ff;">else</span> <span style="color:#0000ff;">if</span> (cn &gt; 0<span style="color:#000000;">) {
        </span><span style="color:#008000;">//</span><span style="color:#008000;"> 当着两个字符 不相等 时,cn向前跳跃到 next[cn] 的位置,去寻找长度更短的相同前后缀。</span>
        cn =<span style="color:#000000;"> next[cn];
    } </span><span style="color:#0000ff;">else</span><span style="color:#000000;"> {
        </span><span style="color:#008000;">//</span><span style="color:#008000;"> cn&lt;=0; 此时说明前面已经没有相同前后缀了,即 cn 已经没办法再跳跃了,
        </span><span style="color:#008000;">//</span><span style="color:#008000;"> 此时 pos 对应的 next[pos] 值为 0 (无相同前后缀)</span>
        next[pos++] = 0<span style="color:#000000;">;
    }
}

</span><span style="color:#0000ff;">return</span><span style="color:#000000;"> next;

}



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





    从上述表格可以看出,无论是之前通过“最长相同前缀后缀长度值右移一位,然后初值赋为-1”得到的next 数组,还是之后通过代码递推计算求得的next 数组,结果是完全一致的。


 


1.3.5 基于《next 数组》匹配


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


                             


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





    在正式匹配之前,让我们来再次回顾下上文2.1节所述的KMP算法的匹配流程:



























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




 


1.3.6 Next 数组与有限状态自动机


    next 负责把模式串向前移动,且当第j位不匹配的时候,用第next[j]位和主串匹配,就像打了张“表”。此外,next 也可以看作有限状态自动机的状态,在已经读了多少字符的情况下,失配后,前面读的若干个字符是有用的。



1.3.7 Next 数组的优化


    至此我们已经实现了KMP算法,但是关于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] ]。总结即是:


如果a位字符与它的next值(即next[a])指向的b位字符相等(即p[a] == p[next[a]]),则a位的next值就指向b位的next值即(next[ next[a] ])。


    所以,咱们得修改下求next 数组的代码。



    public static int[] next(String p) {
int[] next = new int[p.length()];
int k = -1, j = 0;
next[0] = -1; // 初值为-1

    <span style="color:#0000ff;">while</span>(j &lt; p.length() - 1<span style="color:#000000;">) { 
        </span><span style="color:#008000;">//</span><span style="color:#008000;">    p[k]表示字符串的前缀,p[j]表示字符串的后缀</span>
        <span style="color:#0000ff;">if</span>(k == -1 || p.charAt(k) == p.charAt(j)) {  <span style="color:#008000;">//</span><span style="color:#008000;"> 判断的先后顺序不能调换</span>
            k++<span style="color:#000000;">;
            j</span>++<span style="color:#000000;">;
            </span><span style="color:#008000;">//</span><span style="color:#008000;">    后面即是求next[j+1]的过程</span>
            <span style="color:#0000ff;">if</span>(p.charAt(k) == p.charAt(j))             <span style="color:#008000;">//</span><span style="color:#008000;">  此处等价于if(p[j] == p[ next[j] ])
                </span><span style="color:#008000;">//</span><span style="color:#008000;">    因为不能出现p[j] = p[ next[j] ],所以当出现时需要继续递归,k = next[k] = next[next[k]]</span>
                next[j] = next[k];                    <span style="color:#008000;">//</span><span style="color:#008000;">  此处等价于next[j] = next[ next[j] ]</span>
            <span style="color:#0000ff;">else</span><span style="color:#000000;">    
                next[j] </span>=<span style="color:#000000;"> k;
        }
        </span><span style="color:#0000ff;">else</span><span style="color:#000000;"> {
            k </span>=<span style="color:#000000;"> next[k];         
        }
    }

    </span><span style="color:#0000ff;">return</span><span style="color:#000000;"> next;
}</span></pre> 
 </div> 
 <p>  &nbsp;利用优化过后的next 数组求法,可知模式串“abab”的新next数组为:-1 0 -1 0。可能有些读者会问:原始next 数组是前缀后缀最长公共元素长度值右移一位, 然后初值赋为-1而得,那么优化后的next 数组如何快速心算出呢?实际上,只要求出了原始next 数组,便可以根据原始next 数组快速求出优化后的next 数组。还是以abab为例,如下表格所示:</p> 
 <p>&nbsp; &nbsp;&nbsp;<img src="https://www.icode9.com/i/?i=20140801160510832" alt=""></p> 
 <p>&nbsp;</p> 
 <p class="p17">只要出现了p[next[j]]&nbsp;=&nbsp;p[j]的情况,则把next[j]的值再次递归。例如在求模式串“abab”的第2个a的next值时,如果是未优化的next值的话,第2个a对应的next值为<strong>0</strong>,相当于第2个a失配时,下一步匹配模式串会用<strong>p[0]</strong>处的a再次跟文本串匹配,必然失配。所以求第2个a的next值时,需要再次递归:next[2]&nbsp;=&nbsp;next[&nbsp;next[2]&nbsp;]&nbsp;=&nbsp;next[0]&nbsp;=&nbsp;-1(此后,根据优化后的新next值可知,第2个a失配时,执行“如果j&nbsp;=&nbsp;-1,或者当前字符匹配成功(即S[i]&nbsp;==&nbsp;P[j]),都令i++,j++,继续匹配下一个字符”),同理,第2个b对应的next值为0。</p> 
 <p class="p17">对于优化后的next数组可以发现一点:<strong><span style="color:#ff0000;">如果模式串的后缀跟前缀相同,那么它们的next值也是相同的</span></strong>,例如模式串abcabc,它的前缀后缀都是abc,其优化后的next数组为:-1&nbsp;0&nbsp;0&nbsp;-1&nbsp;0&nbsp;0,前缀后缀abc的next值都为-1&nbsp;0&nbsp;0。</p> 
 <p>&nbsp;接下来,咱们继续拿之前的例子说明,整个匹配过程如下:</p> 
 <p>&nbsp; &nbsp;&nbsp;<em>1</em>. S[3]与P[3]匹配失败。</p> 
 <blockquote> 
  <blockquote> 
   <p><img src="http://hi.csdn.net/attachment/201106/14/8394323_130807585714lW.jpg" alt=""></p> 
  </blockquote> 
 </blockquote> 
 <p>&nbsp; &nbsp;<em>&nbsp;2</em>. S[3]保持不变,P的下一个匹配位置是P[next[3]],而next[3]=0,所以P[next[3]]=P[0]与S[3]匹配。</p> 
 <blockquote> 
  <blockquote> 
   <p><img src="http://hi.csdn.net/attachment/201106/14/8394323_13080758571390.jpg" alt=""></p> 
  </blockquote> 
 </blockquote> 
 <p>&nbsp; &nbsp;&nbsp;<em>3</em>.&nbsp;&nbsp;由于上一步骤中P[0]与S[3]还是不匹配。此时i=3,j=next [0]=-1,由于满足条件j==-1,所以执行“++i, ++j”,即主串指针下移一个位置,P[0]与S[4]开始匹配。最后j==pLen,跳出循环,输出结果i - j = 4(即模式串第一次在文本串中出现的位置),匹配成功,算法结束。</p> 
 <blockquote> 
  <blockquote> 
   <p>&nbsp; &nbsp;<img src="http://hi.csdn.net/attachment/201106/14/8394323_1308075857e40u.jpg" alt=""></p> 
  </blockquote> 
 </blockquote> 
 <p>至此对于KMP算法的讲解分析已经全部结束,下面将贴出完整代码供大家阅读:</p> 
 <div class="cnblogs_code"> 
  <pre><span style="color:#0000ff;">public</span> <span style="color:#0000ff;">class</span><span style="color:#000000;"> KMP {

</span><span style="color:#0000ff;">public</span> <span style="color:#0000ff;">static</span> <span style="color:#0000ff;">void</span><span style="color:#000000;"> main(String[] args) {
    String s </span>= "abacababc"<span style="color:#000000;">;
    String p </span>= "abab"<span style="color:#000000;">;
    System.out.println(Index_KMP(s, p));
}

</span><span style="color:#008000;">//</span><span style="color:#008000;">优化过后的next数组求法 </span>
<span style="color:#0000ff;">public</span> <span style="color:#0000ff;">static</span> <span style="color:#0000ff;">int</span><span style="color:#000000;">[] next(String p) {
    </span><span style="color:#0000ff;">int</span>[] next = <span style="color:#0000ff;">new</span> <span style="color:#0000ff;">int</span><span style="color:#000000;">[p.length()];
    </span><span style="color:#0000ff;">int</span> k = -1, j = 0<span style="color:#000000;">;
    next[</span>0] = -1;        <span style="color:#008000;">//</span><span style="color:#008000;">    初值为-1    </span>
    
    <span style="color:#0000ff;">while</span>(j &lt; p.length() - 1<span style="color:#000000;">) { 
        </span><span style="color:#008000;">//</span><span style="color:#008000;">    p[k]表示字符串的前缀,p[j]表示字符串的后缀</span>
        <span style="color:#0000ff;">if</span>(k == -1 || p.charAt(k) == p.charAt(j)) {  <span style="color:#008000;">//</span><span style="color:#008000;"> 判断的先后顺序不能调换</span>
            k++<span style="color:#000000;">;
            j</span>++<span style="color:#000000;">;
            </span><span style="color:#008000;">//</span><span style="color:#008000;">    后面即是求next[j+1]的过程</span>
            <span style="color:#0000ff;">if</span>(p.charAt(k) == p.charAt(j))             <span style="color:#008000;">//</span><span style="color:#008000;">  此处等价于if(p[j] == p[ next[j] ])
                </span><span style="color:#008000;">//</span><span style="color:#008000;">    因为不能出现p[j] = p[ next[j] ],所以当出现时需要继续递归,k = next[k] = next[next[k]]</span>
                next[j] = next[k];                    <span style="color:#008000;">//</span><span style="color:#008000;">  此处等价于next[j] = next[ next[j] ]</span>
            <span style="color:#0000ff;">else</span><span style="color:#000000;">    
                next[j] </span>=<span style="color:#000000;"> k;
        }
        </span><span style="color:#0000ff;">else</span><span style="color:#000000;"> {
            k </span>=<span style="color:#000000;"> next[k];         
        }
    }

    </span><span style="color:#0000ff;">return</span><span style="color:#000000;"> next;
}

</span><span style="color:#0000ff;">public</span> <span style="color:#0000ff;">static</span> <span style="color:#0000ff;">int</span><span style="color:#000000;"> Index_KMP(String S, String P) {
    </span><span style="color:#0000ff;">int</span> i = 0, j = 0<span style="color:#000000;">;
    </span><span style="color:#0000ff;">int</span>[] next =<span style="color:#000000;"> next(P);
    
    </span><span style="color:#0000ff;">while</span>(i &lt; S.length() &amp;&amp; j &lt;<span style="color:#000000;"> P.length()) {      
        </span><span style="color:#0000ff;">if</span>(j == -1 || S.charAt(i) == P.charAt(j)) {        <span style="color:#008000;">//</span><span style="color:#008000;">    如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++. 注意:这里判断顺序不能调换! </span>
            i++<span style="color:#000000;">;
            j</span>++<span style="color:#000000;">;
        }
        </span><span style="color:#0000ff;">else</span>
            <span style="color:#008000;">//</span><span style="color:#008000;">    如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]      
            </span><span style="color:#008000;">//</span><span style="color:#008000;">    next[j]即为j所对应的next值,效果为进行回溯        </span>
            j =<span style="color:#000000;"> next[j];
    }
    
    </span><span style="color:#0000ff;">if</span>(j ==<span style="color:#000000;"> P.length())
        </span><span style="color:#0000ff;">return</span> i -<span style="color:#000000;"> j;
    </span><span style="color:#0000ff;">else</span> 
        <span style="color:#0000ff;">return</span> -1<span style="color:#000000;">;
}

}

1.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的算法流程:





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


 


2. 扩展1:BM算法


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


    BM算法定义了两个规则:



    下面举例说明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算法不仅效率高,而且构思巧妙,容易理解。


 


3. 扩展2:Sunday算法


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


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



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


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


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


    4. 匹配成功。


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


转载于:https://www.cnblogs.com/cherryljr/p/6519748.html

标签:字符,匹配,后缀,模式,next,算法,详解,KMP,失配
来源: https://blog.csdn.net/weixin_43988321/article/details/111644613