KMP模式匹配算法学习笔记

字符串匹配应该是在串中最为重要的内容之一了吧。

可是一直以来苦于智商有限都没有搞懂KMP算法,最近看了很多博客和书籍才总算是完全搞懂。下面就详细总结下学习KMP算法的笔记吧。(图片等数据见参考资料)

环境背景是这样的:有两个字符串S和T,长度分别是n和m。实现一个算法,如果字符串S中含有子串T,则返回T在S中开始的位置,不含有则返回-1。我们先看下朴素的模式匹配算法哈O(∩_∩)O~

 

朴素的模式匹配算法(BF算法)


假设我们要从下面的主串S=“goodgoogle”中,找到T=“google”这个子串的位置。我们通常需要下面的步骤:

其实就是对主串的每一个字符作为子串的开头与要匹配的子串进行匹配。对主串做大循环,每个字符开头做T的长度的小循环,直到匹配成功或者全部遍历完成为止。

朴素的模式匹配算法代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int  BF_Match( char * s, char * t)
{
       int   sLen =  strlen (s);  //获取主串的长度
       int   tLen =  strlen (t);  //获取子串的长度    
 
       int   i = 0;   //i 表示主串当前位置下标
       int   j = 0;   //j 表示子串当前位置下标
 
       while (i < sLen && j < tLen)
        {
             if  (s[i] == t[j])  //如果当前字符匹配成功,则匹配下一个字符
             {
                  i++;
                  j++;
              } else {            //如果当前字符匹配不成功,则子串从头开始与主串的下一个字符开始匹配
                  i = i - j +1;
                  j = 0;   
              }         
 
        }  
         
       if (j == tLen)       //循环结束后,如果j == tLen 表示匹配成功的子串字符个数与子串的长度相等,即匹配成功
           return   i - j;  //匹配成功,返回子串在主串的位置
       else           
           return  -1;      //匹配失败,则返回-1  
     
}

当然这种算法是可行的,只不过就是效率低,很死板,所以朴素的模式匹配算法又叫暴力模式匹配算法。

因为在朴素匹配算法中,很多操作却是不需要的,多于的。比如上图中的2、3、4。为啥呢?

 

因为:S[1]=T[1]

           T[0]≠T[1]

所以:T[0]≠S[1]

 

也就是说就没有必要再让图2这一步比较了,因为结果是已知的嘛。以此类推,图3和4也就是不需要比较的。

这就是朴素匹配算法效率低的本质:回溯,不断的回溯

 

KMP模式匹配算法


KMP算法就是用来解决朴素算法中回溯的问题。所以其本质为:好马不吃回头草

我们完全可以根据子串自身的特点,找到一种方法实现好马不吃回头草的能力。

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

KMP的算法流程如下:

    假设现在文本串S匹配到 i 位置,模式串T匹配到 j 位置
           如果j  = -1或者当前字符匹配成功(即S[i] == T[j]),都令i++,j++,继续匹配下一个字符;
           如果j != -1或者当前字符匹配失败(即S[i] != T[j]), 则令 i 不变,j = next[j]。此举意味着失配时,模式串T相对于文本串S向右移动了j - next [j] 位。

PS: j = -1 是因为任何模式串的next值都等于=-1,而且也只有首字符的next值才会等于-1,后面有讲到。        

          换言之,当匹配失败时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值(next 数组的求解会在后面详细阐述),

           即移动的实际位数为:j - next[j],且此值一定是大于等于1的,这样子串就具备了有不回溯的能力了。

 

现在我们就可以总结下next 数组值的含义了:

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

于是乎,假如在我们已经搞定了next数组的前提下。可以写出KMP算法的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int  KMP_Match( char * s, char * t)
{
       int   sLen =  strlen (s);  //获取主串的长度
       int   tLen =  strlen (t);  //获取子串的长度    
       int   i = 0;   //i 表示主串当前位置下标
       int   j = 0;   //j 表示子串当前位置下标
       while (i < sLen && j < tLen)
        {
             if  (j ==-1 || s[i] == t[j])  //如果当前字符匹配成功,则匹配下一个字符
             {
                  i++;
                  j++;
              } else {           
                    j = next[j];   //如果当前字符匹配不成功,则j退回合适的位置,i值不变
              }          
       
        
       if (j == tLen)              //j == tLen 表示匹配成功的子串字符个数与子串的长度相等,即匹配成功
             return   i - j;  //匹配成功,返回子串在主串的位置
       else            
             return  -1;      //匹配失败,则返回-1    
}

咦,代码好熟悉,好像在哪儿见过。。不就是在朴素算法中去掉了i值回溯部分嘛O(∩_∩)O~

此也意味着在某个字符失配时,该字符对应的next 值会告诉你下一步匹配中,模式串应该跳到哪个位置(跳到next [j] 的位置)。

 

好了,是时候计算next数组值了!!

第一步:寻找最长前缀后缀

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

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


第二步:根据《最大长度表》求解next数组值

把最大长度表整体右移一位,然后把初值赋为-1。也就可以得到如下图:


第三步:根据next数组值完整的来次说匹配就匹配的KMP全过程

给定:文本串S= BBC ABCDAB ABCDABCDABDE

           模式串P= ABCDABD

开始吧

在正式匹配之前,让我们来再次回顾上面所述的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]匹配失败

      • 因为刚开始匹配i = j =0,所以执行“如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]”,查表得next[0] = -1,所以j =  -1,

      • 故转而执行“如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++”,得到i = 1,j =  0。这一步相当于向右移动:j - next[j] = 0 - (-1)=1位。即P[0]继续跟S[1]匹配。

    • P[0]跟S[1]又失配,执行j=next[j]=next[0]=-1,所以 j 再次等于-1,i、j继续自增,从而P[0]跟S[2]匹配。这一步相当于向右移动:j - next[j] = 0 - (-1)=1位。

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

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

  • 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 位。因为:j - next[j] = 0 - (-1)=1位。

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

  • 6. 匹配成功,然后i++,j++,循环结束。

    循环结束后,此时i = 22,j = 7,所以返回i - j = 15,即模式串在文本串下标为15的位置匹配成功。

 

终于匹配完了,KMP是不是很强大啊。当然主要功臣肯定是next数组啦。

下面是通过递推方式求解next数组的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void  Get_Next( char * p, int  next[])  
   {  
     int  pLen =  strlen (p);   //获取模式串的长度
     next[0] = -1;           //任何模式串的首字符的next数组值都等于-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];  
            }  
       }  
   }

从最开始的BF算法时间复杂度为O(m*n)到KMP算法O(m+n),如果你觉得这就是极限,那就错了。

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

Daniel M.Sunday在1990年提出Sunday算法,它的思想跟BM算法很相似,但在实际操作中,Sunday算法效率更高。

以后有机会再好好学学BM算法和Sunday算法。


参考资料:

1、 http://blog.csdn.net/v_july_v/article/details/7041827
2、《大话数据结构》程杰 第5章
3、《算法导论》第三版 第32章

你可能感兴趣的:(算法与数据结构)