KMP算法

前言:算法并不是很难,只是会让初学者感觉有点绕,其实并不是算法绕,而是匹配这个问题本身绕。

假设你对简单的字符串模式匹配过程是理解的。


0、KMP算法简介

要解决的问题是:找出字符串T在字符串S中的位置。

主要思路:当在比较过程中出现失配字符时,主串下标不回溯,模式串根据当前已经匹配的字符串把下标回退到位置k,然后继续下一轮的匹配。

正如来自wikipedia的定义:

In computer science, the Knuth–Morris–Pratt string searching algorithm (or KMP algorithm) searches for occurrences of a "word" W within a main "text string" S by employing the observation that when a mismatch occurs, the word itself embodies sufficient information to determine where the next match could begin, thus bypassing re-examination of previously matched characters

1、相关定义

  • 把S称为主串,用 i 表示S中字符的下标,S[i]表示主串中下标为i的字符;
  • 把T称为模式串,用 j 表示T中字符的下标, T[j表示主串中下标为j的字符;
  • 本文中提到的下标均是从0开始。下标前移指的是下标数值减小,下标后移指的是下标增大。
  • 子串在主串的位置默认为子串第一次在主串中出现时第一个字符在主串中的下标。
  • 真前缀子串(proper prefixes):不包含最后一个字符的前缀子串。如:“S”, “Sn”, “Sna”, and “Snap” 是 “Snape”的真前缀串.
  • 真后缀串(proper suffixes):不包含第一个字符的后缀子串。如:“agrid”, “grid”, “rid”, “id”, and “d” 是“Hagrid”的真后缀串.
  • T(k:m)表示T中下标从k到m的子串。

2、算法描述

//在开始之前,需要清楚的是:当出现以下两种情况任意一种时,i 会后移:
//情况一:与模式串第一个字符失配;情况二:与模式串中当前字符匹配
输入:模式串,主串
输出:模式串第一次在主串中出现的位置
(1)初始化:i=0,j=02)判断 S[i]==T[j] 是否成立,若成立,i和j同时后移一位,若不成立,即出现了失配字符,转步骤(3);
(3)此时分如下几种情况分析:
  若j==0, i后移;
  若j>0,则需要把j前移。方法是:首先,找T(0:j-1)最长的相同真前缀和真后缀,
      若找到,j的值就是这个满足条件的真前缀后面那个字符的下标;
      若没找到,j的值为0,即回到T首。
(4)开始下一轮匹配,重复步骤(2),直到S或T的所有字符都参与比较。

 上述步骤中的难点就是步骤(3)。

这一步要解决的问题是当失配的时候,应该把模式串的下标回退到哪里。而再看步骤(3)不难发现,回退的位置只跟T自身有关系。

那么我们可以先准备好一个表格,里面记录在下标 j 处失配时,应该回退到k处继续进行下一轮匹配。这个表格就是next数组。

如:

模式串T:  "abcadeabca"

next[]:     -1  0  0  0  1  0  0  1  2  3

注:其中,next[0]=-1主要是对应上文中提到的主串下标后移的第一种情况,即匹配过程中在模式串首失配,显然这时T的下标应该继续保持在串首。

为了统一使用“i++; j++”的处理方式,这样,主串下标后移一位后,确保模式串下标在第一个(即j=0)。

3、C语言实现

 1 int samePreSufLen(char *p,int m) //找出p[0:m]中(最长相同真前缀串和真后缀串)的长度
 2 {
 3     int temp;
 4     int i;
 5     temp=m;
 6     while(temp>0)
 7     {
 8         for(i=0;i<temp;i++)
 9             if(p[i]!=p[m-temp+1+i])
10                 break;
11         if(i==temp)
12             return temp;
13         temp--;    
14     }
15     if(temp==0)
16         return 0;
17 }
18 //////////////////////////////////////////////////
19 void getNext(char *t,int *next)
20 {
21     int i;
22     for(i=0;i<t.len;i++)
23     {
24         if(i==0)
25             next[i]=-1;     //next[0]=-1
26         else if(i==1) 
27             next[i]=0;      //next[1]=0
28         else
29             next[i]=samePreSufLen(t,i-1);
30     }
31 
32     printf("next[]:");
33     for(i=0;i<t.len;i++)
34      printf("%d  ",next[i]);
35     printf("
");
36 }
37 ////////////////////////////////////////////////////
38 int StrIndexKMP(char *s,char *t) // KMP算法
39 {  
40     int i=0,j=0;
41     int *next;
42         slen=strlen(s);
43         tlen=strlen(t);
44     if(slen < tlen)
45         return -1; //模式匹配不成功
46     next=(int*)malloc(sizeof(int)*tlen);
47     getNext(t,next);
48     while(i<slen && j<tlen) 
49     {   
50         if(j==-1 || s[i] == t[j])      //继续匹配下一个字符
51         {
52             i++; 
53             j++;
54         }
55         else  //模式串指针回溯准备下一次匹配
56             j=next[j];
57     }
58     if (j==tlen)  
59         return(i-tlen); //返回匹配的第一个字符的下标
60     else 
61         return -1; //模式匹配不成功
62 }     

4、一些关于next数组的改进方法

其实,关于next数组还有不少优化方法值得拿来改进算法,比如:

当next[k]==0 且 T[0]==T[k]时,意味着模式串下标从失配字符下标回退到0依然会失配,因此可以在遇到这种情况时,直接把next[k]赋值为-1.

参考文献:

[1] wikipedia: https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm

[2] http://jakeboxer.com/blog/2009/12/13/the-knuth-morris-pratt-algorithm-in-my-own-words/

原文地址:https://www.cnblogs.com/wxiaoli/p/7609468.html