串的模式匹配-KMP详解

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/lz161530245/article/details/78626363

前两个星期在数据结构上学习了KMP,一听说要学KMP就感觉好难,因为上学期在没学数据结构之前实验室有个学姐讲过KMP算法,听的真是云里雾里,不知道next数组到底是干啥的,下去自己也没有学习,所以在前两个星期学到KMP时,很认真的学,终于在老师快讲完这一章的时候听明白了,但感觉不是太熟练,当时也没有时间写博客整理一下思路,所以这就导致了这周一分享KMP时,思想一点就通,但是具体步骤还是理解的不是很好,这两天课多事多,下去仍旧没有细看,但是这周的专题有KMP,趁星期五,刚才花了两个小时彻底弄懂了它,原来只要静下心来学习,还是能理解的嘛,哈哈哈。借用了很多大神的讲解,添加一些自己的想法,好好的整理一下KMP算法的思想和实现过程。


       设有主串s子串t,子串t的定位就是要在主串s中找到一个与子串t相等的子串。通常把主串s称为目标串,把子串t称为模式串,因此定位也称作模式匹配。

      模式匹配成功是指在目标串s中找到一个模式串t;不成功则指目标串s中不存在模式串t。 

模式匹配的应用非常广泛。例如,在文本编辑程序中,我们经常要查找某一特定词

在文本中出现的位置。显然,解此问题的有效算法能极大地提高文本编辑程序的响应

性能。模式匹配是一个较为复杂的串操作过程。迄今为止,人们对串的模式匹配提出了

许多思想和效率各不相同的计算算法。在分析KMP算法之前,我们先来了解一下BF算

。(这样更能衬托出KMP的高效,哈哈)


一:Brute-Force算法

Brute-Force简称为BF算法,亦称简单匹配算法,其基本思路是:

      从目标串s=“s0s1…sn-1”的第一个字符开始和模式串t=“t0t1…tm-1”中的第一个字符比较,若相等,则继续逐个比较后续字符;否则从目标串s的第二个字符开始重新与模式串t的第一个字符进行比较。依次类推,若从模式串s的第i个字符开始,每个字符依次和目标串t中的对应字符相等,则匹配成功,该算法返回i(i是目标串中与模式串第一个字符匹配成功的位置);否则,匹配失败,函数返回-1。

例如,设目标串s=“cddcdc”,模式串t=“cdcs的长度为n(n=6),t的长度为m(m=3)。用指针i指示目标串s的当前比较字符位置,用指针j指示模式串t的当前比较字符位置。BF模式匹配过程如下所示。 


第一次匹配:s=c d d c d c       i=2         失败

t =c d cj=2

第二次匹配:s=c d d c d c       i=1        失败

t = c d cj=0

第三次匹配:s=c d d c d c       i=2        失败

t =      c d c          j=0

第四次匹配:s=c d d c d c       i=5       成功

t =         c d c       j=2


int index(SqString s,SqString t)
{  int i=0,j=0,k;
   while (i<s.length && j<t.length) 
   {  if (s.data[i]==t.data[j])//继续匹配下一个字符
	 {  i++; j++;  }  //主串和子串依次匹配下一个字符
	 else          //主串、子串指针回溯重新开始下一次匹配
	 {  i=i-j+1; 	    //主串从下一个位置开始匹配
         j=0;           //子串从头开始匹配
      }
   }
   if (j>=t.length) 
        k=i-t.length; //返回匹配的第一个字符的下标
   else  
        k=-1;        	//模式匹配不成功
} 

这个算法简单,易于理解,但效率不高,主要原因是:主串指针i在若干个字符序列比较相等后,若有一个字符比较不相等,仍需回溯(i=i-j+1)

      该算法在最好情况下的时间复杂度为O(m),即主串的前m个字符正好等于模式串的m个字符。在最坏情况下的时间复杂度为O(n*m) 

理解该算法的关键点是

      当第一次si≠tj时:主串要退回(即回溯)到i-j+1的位置,而模式串也要退回到第一个字符(即j=0的位置)。

 但是,我们知道当比较出现si≠tj时:应该有si-1=tj-1,…,si-j+1=t1, si-j=t0 ,即已经出现有“部分匹配”的结果。

我们能不能利用这些部分匹配信息,消除主串的回溯以提高算法的效率呢

例:设有串s=“abacabab” ,t=“abab” 。则第一次匹配过程如下例所示。

 s=“a  b  a c  a  b a  b”          i=3

       || ||  ||  ≠                    匹配失败

 t=“a  b  a b”                         j=3

在i=3和j=3时,匹配失败。但重新开始第二次匹配时,不必从i=1 ,j=0开始。因为s1=t1,t0≠t1,必有s1≠t0,又因为s2=t2, t0=t2,所以必有s2=t0。由此可知,第二次匹配可以直接从i = 3 、j = 1开始。

下面我们就开始介绍KMP算法啦,注意啦,注意啦!!!

二:KMP算法

KMP算法是D.E.Knuth、J.H.Morris和V.R.Pratt共同提出的,简称KMP算法。该算法较BF算法有较大改进,主要是消除了主串指针的回溯,使算法效率有了某种程度的提高。 

每当一趟匹配过程出现字符不相等时,主串指示器不用回溯,而是利用已经得到的“部分匹配”结果,将模式串的指示器向右“滑动”尽可能远的一段距离后,继续进行比较。

例如,设目标串s=“BBC  ABCDAB  ABCDABCDABDE”,模式串t=“ABCDABD。我们想知道目标串里是否包含有模式串。

1.

首先,字符串"BBC ABCDAB ABCDABCDABDE"的

第一个字符与搜索词"ABCDABD"的第一个字符,进

行比较。因为B与A不匹配,所以搜索词后移一位

2.

因为B与A不匹配,搜索词再往后移。

3.

就这样,直到字符串有一个字符,与搜索词的第一个

字符相同为止。

4.

接着比较字符串和搜索词的下一个字符,还是相同。

5.

直到字符串有一个字符,与搜索词对应的字符不相同

为止。

6.

这时,最自然的反应是,将搜索词整个后移一位,再

从头逐个比较。这样做虽然可行,但是效率很差,因

为你要把"搜索位置"移到已经比较过的位置,重比一

遍。

7.

一个基本事实是,当空格与D不匹配时,你其实知道

前面六个字符是"ABCDAB"。KMP算法的想法是,设

法利用这个已知信息,不要把"搜索位置"移回已经比

较过的位置,继续把它向后移,这样就提高了效率。

8.

9.

已知空格与D不匹配时,前面六个字符"ABCDAB"是

匹配的。查表可知,最后一个匹配字符B对应的"部分

匹配值"为2,因此按照下面的公式算出向后移动的位

数:

  移动位数 = 已匹配的字符数 - 对应的部分匹配值

因为 6 - 2 等于4,所以将搜索词向后移动4位。

10.

因为空格与C不匹配,搜索词还要继续往后移。这

时,已匹配的字符数为2("AB"),对应的"部分匹配

值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将

搜索词向后移2位。

11.

因为空格与A不匹配,继续后移一位。

12.

逐位比较,直到发现C与D不匹配。于是,移动位数

 = 6 - 2,继续将搜索词向后移动4位。

13.

逐位比较,直到搜索词的最后一位,发现完全匹配,

于是搜索完成。如果还要继续搜索(即找出全部匹

配),移动位数 = 7 - 0,再将搜索词向后移动7位,

这里就不再重复了。

14.

"部分匹配值"就是"前缀"和"后缀"的最长的共有元素

的长度。以"ABCDABD"为例,

  - "A"的前缀和后缀都为空集,共有元素的长度为0;

  - "AB"的前缀为[A],后缀为[B],共有元素的长度为0;

  - "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;

  - "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;

  - "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;

  - "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;

  - "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。

15.

"部分匹配"的实质是,有时候,字符串头部和尾部会

有重复。比如,"ABCDAB"之中有两个"AB",那么它

的"部分匹配值"就是2("AB"的长度)。搜索词移动

的时候,第一个"AB"向后移动4位(字符串长度-部分

匹配值),就可以来到第二个"AB"的位置。


经过上面的讲解,相信大家都应该很明白了吧,这是我借鉴大牛写的,链接:点击打开链接,感觉他写的很容易明白,那么我们来看代码实现部分吧!

/**这里先要说清楚几点,第一点就是next[i]是用来存前i个字符的相同的最大前缀和最大后缀的长
第二点就是我是以-1开头的,当next数组为-1的时候,说明没有匹配的,为0的时候有一个匹配的等等,
因为我每次的判断就是b[k+1]来判断的,第三点就是这个变量k,它是用来回溯用的。**/
void get_next(char b[],int blen,char next[])/// 求出模式串的next数组
///传模式串和模式串的长度,以及next数组
{
    next[0] = -1;///第一个因为没有比较,我们初始他为-1
    int k = -1;
    ///初始一个变量为-1,当字符串匹配到某一个不相等的时候,该变量用来记录next数组值,往前回溯
    for (int i = 1; i < blen; i++)
    {
        while (k > -1 && b[k + 1] != b[i])///如果匹配到某一个不相等的时候,开始回溯
        {
            k = next[k];///回到next数组中记录的位置重新匹配
        }
        if (b[k + 1] == b[i])///如果字符一样,继续向前匹配
        {
            k = k + 1;
        }
        next[i] = k;///将最大匹配的值赋给next数组
    }
}

int KMP(char a[],char b[],int alen,int blen)///KMP算法
///传入的参数是主串a,模式串b,主串a的长度,模式串b的长度
{
    get_next();///先的到模式串的next数组,上面已经实现过
    int k = -1;///k依然从-1开始
    for (int i = 0; i < n; i++)///将主串每个字符遍历一遍
    {
        while (k >-1&& b[k + 1] != a[i])///匹配到某一字符,突然不匹配了
        {
            k = next[k];///回溯
        }
        if (b[k + 1] == a[i])///如果匹配,继续向前
            k = k + 1;
        if (k == m-1)///如果匹配完模式串,则返回模式串在主串中匹配的初始位置
        {
            return (i-m+1);
        }
    }
    return -1;///如果没有在主串中找到与模式串匹配的,则返回-1
}

好啦,讲解就是介么多啦,宝宝们可以参考啦,有怀疑的地方尽管提出,有错的地方也望告诉我,好及时修改!





展开阅读全文

没有更多推荐了,返回首页