KMP算法—最通俗易懂的讲解!

引言

我们把寻找字符串A(模式串)在字符串B(主串)第一次完整地出现的位置,把这个过程叫做字符串匹配,例如下图:

KMP算法—最通俗易懂的讲解!_第1张图片
在这种模式匹配中,最粗暴简单的方法:

  • 开始之前记个k=0作为匹配失败的次数,i作为模式串比较字符的位置,j作为主串比较字符的位置;
  • 匹配成功时,接续向下一个字符进行匹配,直到匹配模式串匹配完成;
  • 当发现匹配失败时,模式串重新到首个字符,而主串则回溯到k+1的位置,k自加1;
    KMP算法—最通俗易懂的讲解!_第2张图片

![image](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9pbWcuYWxpY2RuLmNvbS9pbWdleHRyYS9pMi8yNTk2NTMzOTcyL08xQ04wMU5ZN1ZWeDFmRERqWGkya1R1XyEhMjU5NjUzMzk3Mi5wbmc?x-oss-process=image/format,png)

KMP算法—最通俗易懂的讲解!_第3张图片

这种方法是最简单的,但同时也是最低效的:因为在匹配的过程中,需要同时回溯i,j两个参数;但是这种回溯大部分都是多余的;为了解决这种低效,1977由Donald Knuth、Vaughan Pratt、James H. Morris三人于年联合了一个字符串模式匹配算法,Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”。

代码实现:
int index(Str str,Str substr)
{
    int i=1,j=1,k=i;
    while(i <= str.length && j<= substr.length)
    {
        if (str.ch[i]==substr[j])
        {
            ++i;
            ++j;
        }
        else
        {
            j = 1;
            i =++k; #匹配失败; 
        }   
    }
    if(j > substr.length)
    {
        return k;
    }
    else
    {
        return 0;
    }
}

KMP算法的匹配流程

假设主串S匹配到j的位置,模式串P匹配到i的位置,还没匹配结束,继续匹配下去会出现下面两种情况:

  • 1,当P[i] == S[j]时,则继续匹配 i++,j++;
  • 2,当P[i] != S[j]时,则 i =next[i],j++,此时等价于模式串P向右移动了 i -next[i]个单位;

以上只是带大家初步认识了KMP算法的基本匹配思想,下面则需要进行深入地了解一下next数组,以及对匹配思想的理解;

第一步,字符串相同的前后缀元素

在理解next数组之前,需要直到next数组是怎么求出来的,首先我们看一下下面的这张图

KMP算法—最通俗易懂的讲解!_第4张图片

图中我们了解到几个信息:

  • S为主串;P为模式串;
  • 图中的S[i-k] — S[i]部分与P中的P[j-k]到P[j](模式串P的前面)部分相同,同时也与P[0]到P[k](模式串P的前面)相同;
  • 模式串向后移动了j-next[j]个单位;

从以上几部分信息,可以这样理解:

  • 在P未移动之前之前有部分连续字符串与S已经匹配成功了,同时P的前面一小部分也能匹配成功,所以直接可以把P的首个字符移动到与后面字符串重复地方的开始字符,也就是P[j-k];


  • 因为只是部分连续,所以在移动的过程捕获出现字符串匹配成功的可能。

上面重复的部分就是模式串的相同前后缀的最大长度,也就等于next函数,而移动的距离刚好是 j(匹配失败的位置) - next[j](前面字符串前后缀重复的最大长度)

字符串重复前后缀的最大长度也就是字符串前面的连续字符(从第一个开始)有多少个与后面的相同,自身与自身不算同一个。如下图:
KMP算法—最通俗易懂的讲解!_第5张图片
也就能求出next数组,只不过是关于后一字符的;

第二步,已知next[1]....next[j],怎样求得next[j+1]

在第一步中,已经分析过了,next数组的值与前后缀的最大长度相关,而关于最大长度得出的next值 是后一字符的next数组的值;

这里,有一种比较权威的求next[j+1]的方法(这里是以第一个字符未p[0]为依据的):

  • next[0] = -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],而后重复此过程。

注意到没,k字符在这里起到的是最大长度值的作用,例如下面这张图,

  • 当j = 2时,k =0, p[k]= A 则因为p[3]!=p[k],所以 k =next[k]一直递归到 k =-1,则next[3] =k+1 =0;
  • 依次类推我们可以了解到当 p[j] =D 时,next[j] =k +1 =2 ;
  • 当p[j+1] = E时,next[j+1] = next[next[k]] +1 = 0;
    KMP算法—最通俗易懂的讲解!_第6张图片

总结

整个分析到这里基本上也就结束了,整个KMP算法的关键核心是求next函数,next函数需要我们联系到模式串的前后缀最大长度值。KMP算法虽然在字符串的匹配过程中著以高效,但依然存在着自己的一些弊端,一些大佬在此基础之上又进一步地对算法进行了优化。

KMP算法代码实现:

void getnext(Str substr,int next[])
{
    int i = 1,j = 0;
    next[1] = 0;
    while(i < substr.length)
    {
        if(j==0||substr.ch[i] == str.ch[j])
        {
            i++;
            j++;
            next[i] = j;
        }
        else
        {
            j =next[j]
        }
    }
}

int KMP(Str str, Str substr, int next[])
{
    int i=1,j=1;
    while(i<= str.length && j <= substr.length)
    {
        if(j == 0 || str.ch[i] == substr.ch[j])
        {
            ++i;
            ++j;
        }
        else
        {
            j =next[j];
        }
        
    }
    if(j > substr.length)
    {
        return i -substr.length;
    }
    else
    {
        return 0;
    }
}

关于优化的过程主要是next函数向nextval函数的转变,关于优化方法,我会在以后单写一篇文章用来讲述,敬请关注!

你可能感兴趣的:(算法)