KMP算法理解

作者: 柠檬乌冬面 | 来源:发表于2017-03-12 14:11 被阅读127次

文章大纲:
1.KMP算法概念
2.KMP算法中最核心的next[] 数组是如何生成的
3.使用KMP算法 匹配字符串 java实现
4.再次梳理记忆和理解KMP算法

KMP算法概念:

假设现在我们面临这样一个问题:有一个文本串S,和一个模式串P,现在要查找P在S中的位置,怎么查找呢?
如果用暴力匹配的思路,并假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置,则有:
如果当前字符匹配成功(即S[i] == P[j]),则i++,j++,继续匹配下一个字符;
如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0。

理清楚了暴力匹配算法的流程及内在的逻辑,咱们可以写出暴力匹配的代码,如下:

<pre>
<code>

  • int ViolentMatch(char* s, char* p)
    · {

  • int sLen = strlen(s);
    
  • int pLen = strlen(p);
    
  • int i = 0;
    
  • int j = 0;
    
  •   while (i < sLen && j < pLen)
    
  • {
    
  •     if (s[i] == p[j])
    
  •     {
    
  •         //①如果当前字符匹配成功(即S[i] == P[j]),则i++,j++
    
  •         i++;
    
  •         j++;
    
  •     }
    
  •     else
    
  •     {
    
  •         //②如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0
    
  •         i = i - j + 1;
    
  •         j = 0;
    
  •     }
    
  • }
    
  • //匹配成功,返回模式串p在文本串s中的位置,否则返回-1
    
  • if (j == pLen)
    
  •     return i - j;
    
  • else
    
  •     return -1;
    
  • }
    </pre>
    </code>

比如:你看当字符串匹配到这里的时候 此时不匹配了。

按照暴力的方法 上面一串将回到第5个位置 下面那一串回到0位置进行重新比较。


此时肯定是不匹配的,因为在前面的匹配过程中 已经按照顺序匹配成功才会走到最后D位置 ,由此可以推理得到第一个A肯定不能和它的下一个也就是B相匹配。

KMP算法就是 利用之前已经部分匹配这个有效信息,保持i(大串的坐标) 不回溯,通过修改j(待匹配串的坐标) 的位置,让模式串尽量地移动到有效的位置。

整个的算法流程:

假设现在文本串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 值(next 数组的求解会在下文的3.3.3节中详细阐述),即移动的实际位数为:j - next[j],且此值大于等于1。
<pre>
<code>

/**

  • 匹配相应的字符串 Kmp
  • @param a 总字符串
  • @param b 待匹配字符串
  • @return 匹配成功返回 字符串在总串中的起始位置 失败返回-1
    */

int Kmpsearch(String a,String b){

char [] chars_1=a.toCharArray();
char [] chars_2=b.toCharArray();

int len_1=a.length();
int len_2=b.length();
this.next=getNext(b);//根据b要匹配的串 生成相应的next数组
int i=0,j=0;
while (i<len_1&&j<len_2){
    if (j==-1||chars_1[i]==chars_2[j]){
        i++;
        j++;
    }else {
        j=next[j];
    }
}
if (j==len_2){
    return i-j;
}else {
    return -1;
}

</pre>
</code>

接下来我们就只需要去考虑如何生成next数组即可

KMP算法中最核心的next[] 数组是如何生成的:

我们先来假设一下next[j]=k 那么这是什么意思呢?
意思就是在第j个位置之前的所有字符串中存在前缀后缀相等的字符串长度为k,所以如果当你匹配到第j个位置是匹配失败,那么你直接使用next去求得k位置重新进行和总字符串这个同一位置比较时,就把前面相同匹配过的过程给节约了。因为你的前后缀相同。如果解释不明白,后面还会有图帮助理解。

下面这张图是来理解前缀后缀的概念:

我们用一个网上的例子进行讲解next的生成:

如下图所示,假定给定模式串ABCDABCE,且已知next [j] = k(上面说过这个的含义 那么在这里 我们可以看到就是Pj之前 ABCDAB的最大相同前后缀为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”。换言之,当pk != pj后,字符E前有多大长度的相同前缀后缀呢?很明显,因为C不同于D,所以ABC 跟 ABD不相同,即字符E前的模式串没有长度为k+1的相同前缀后缀,也就不能再简单的令:next[j + 1] = next[j] + 1 。所以,咱们只能去寻找长度更短一点的相同前缀后缀。


用什么方法去寻找更短一点的相同前后缀呢?
递归前缀索引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],直到要么找到长度更短的相同前缀后缀,要么没有长度更短的相同前缀后缀。如下图所示:

这幅图一开始我也没太认真去看,觉得有点复杂。后面仔细想了一下回过头再看确实对理解k=next[k]有很大的帮助

我的个人理解是这样的:
首先我们可以看到Pj位置,前面有两块相同的k块,在图片上方用大括号括起来代表相同的前后缀长度为k 用公式表示就是next[j]=k ,
然后此时Pk和Pj不相同。一块红色一块黄色。我们要去寻找短的前缀,
根据k=next[k]我们到了Pnext[k]位置 也就是Pk前面存在的最大相同前缀。
看前半部分,是两块蓝色的相等部分。然后此时去比较Pnext[k]和Pj看是否相同,如果相同就有next[j+1]=next[k]+1,
那么为什么j+1的位置前面会有next[k]+1的相同前后缀呢。我们看图,两块k区域是相等的,然后Pk前面的两块蓝色是相等的,所以四块蓝色的区域也就相等了。
那么P0到Pnext[k]这一块也就和Pj相邻的蓝色块相等。
如果一直找到头都没有短一点的前后缀,则next[j+1]=0

现在我们再总结一下next数组的生成,假设:next[j]=k 那么next[j+1]根据Pk和Pj是否相同有两种情况,如果相同 则next[j+1]=k+1,如果不相同则k=next[k]继续去判断此时的Pk和Pj是否相同。
我们初始化化赋值为 j=0 k=-1 next[0]=-1 下面用代码写一遍:
<pre>
<code>

/**

  • 获取next数组的函数
  • @param string 模式串 待匹配的字符串
  • @return 生成的next函数
    */

public int [] getNext(String string){

char [] chars=string.toCharArray();
int len=string.length();
int k=-1;
int j=0;
int []next=new int[len];
next[0]=-1;
while (j<len-1){
    if (k==-1||chars[j]==chars[k]){//chars[j]前缀 chars[k]后缀 相等 或者k==-1
       next[++j]=++k;
    }else {
        k=next[k];//当不匹配时 去前面最大匹配的位置那再次进行比较看是否能匹配
    }
}
return next;

}

</pre>
</code>

next数组优化

如果用之前的next 数组方法求模式串“abab”的next 数组,可得其next 数组为-1 0 0 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] ]。

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

<pre>
<code>

/**

  • 获取next数组的函数
  • @param string 模式串 待匹配的字符串
  • @return 生成的next函数
    */

public int [] getNext(String string){

char [] chars=string.toCharArray();
int len=string.length();
int k=-1;
int j=0;
int []next=new int[len];
next[0]=-1;
while (j<len-1){
    if (k==-1||chars[j]==chars[k]){//chars[j]前缀 chars[k]后缀 相等 或者k==-1
        j++;
        k++;
        if (chars[j]!=chars[k]){//如果此时你让 next[j]=k 那么进行位移的时候 把chars[k]拿去匹配还是失败 因为chars[j]已经失败过
            next[j]=k;
        }else {  //因为不能出现p[j] = p[next[j]],所以当出现时需要继续递归,k = next[k] = next[next[k]]
            next[j]=next[k];
        }
    }else {
        k=next[k];//当不匹配时 去前面最大匹配的位置那再次进行比较看是否能匹配
    }
}
return next;

}

</pre>
</code>

使用KMP算法 匹配字符串 java实现

<pre>
<code>

/**

  • 获取next数组的函数
  • @param string 模式串 待匹配的字符串
  • @return 生成的next函数
    */

public int [] getNext(String string){

char [] chars=string.toCharArray();
int len=string.length();
int k=-1;
int j=0;
int []next=new int[len];
next[0]=-1;
while (j<len-1){
    if (k==-1||chars[j]==chars[k]){//chars[j]前缀 chars[k]后缀 相等 或者k==-1
        j++;
        k++;
        if (chars[j]!=chars[k]){//如果此时你让 next[j]=k 那么进行位移的时候 把chars[k]拿去匹配还是失败 因为chars[j]已经失败过
            next[j]=k;
        }else {  //因为不能出现p[j] = p[next[j]],所以当出现时需要继续递归,k = next[k] = next[next[k]]
            next[j]=next[k];
        }
    }else {
        k=next[k];//当不匹配时 去前面最大匹配的位置那再次进行比较看是否能匹配
    }
}
return next;

}

/**

  • 匹配相应的字符串 Kmp
  • @param a 总字符串
  • @param b 待匹配字符串
  • @return 匹配成功返回 字符串在总串中的起始位置 失败返回-1
    */

int Kmpsearch(String a,String b){

char [] chars_1=a.toCharArray();
char [] chars_2=b.toCharArray();

int len_1=a.length();
int len_2=b.length();
this.next=getNext(b);//根据b要匹配的串 生成相应的next数组
int i=0,j=0;
while (i<len_1&&j<len_2){
    if (j==-1||chars_1[i]==chars_2[j]){
        i++;
        j++;
    }else {
        j=next[j];
    }
}
if (j==len_2){
    return i-j;
}else {
    return -1;
}

}

</pre>
</code>

再次梳理记忆和理解KMP算法

KMP算法的使用过程:两个待比较的字符串进行单个字符一一比较,如果相等则进行下一个位置的比较,否则模式串(小的那串)的位置移动到next[当前位置]。

关键的next数组生成:
有两个指针 j和k。 当k=-1或者在J和K位置的字符相等时,两指针同时向前一位。否则k回退到next[k]位置(寻找更短的前后缀相等位置)。若两指针同时向前一位时 ,比较新的J和K位置字符是否相等,若相等则next[j]=next[k] ,不等 next[j]=k.

文章参考来源:
从头到尾彻底理解KMP

相关文章

  • 对KMP算法的一些理解

    最近学到KMP算法,下面讲讲对KMP算法的一些个人理解,希望对大家有帮助! 对于KMP算法的理解: 整个KMP算法...

  • KMP算法学习札记

    参考文章 知乎:如何更好的理解和掌握 KMP 算法?从头到尾彻底理解KMPKMP 算法(1):如何理解 KMP(原...

  • KMP算法 理解与实现

    KMP算法,背景不必多说,主要想写一写自己对KMP算法的一些理解和其具体实现。关于KMP算法的原理,阮一峰老师的这...

  • 深入理解KMP算法

    深入理解KMP算法 时间:20180313 KMP算法的核心是 求公共最大前后缀。 画图分析 继续分析如何利用前后...

  • KMP算法

    参考文献1.B站灯笼哥2.KMP算法python实现3.如何更好的理解和掌握 KMP 算法?

  • 数据结构与算法14-字符串匹配与KMP

    什么是KMP KMP算法是在字符串匹配算法中比较绕的.主要是需要理解KMP中next数组求解的必要性以及j 的回溯...

  • 字符串匹配问题-KMP算法

    什么是KMP KMP算法是在字符串匹配算法中比较绕的.主要是需要理解KMP中next数组求解的必要性以及j 的回溯...

  • KMP 专题整理

    KMP 学习记录 kuangbin专题十六——KMP KMP 学习总结 朴素 KMP 算法 拓展 KMP 算法(E...

  • 理解 KMP 算法

    1. 概述 字符串是编程中常用的一种数据结构,在各个方面都有广泛的应用,而字符串的一种基本操作就是给定一段长度为N...

  • 理解KMP算法

    简单理解KMP 本文读者可以获得两方面的知识 直观理解如何生成部分匹配表(下文简称匹配表),这是KMP算法的核心思...

网友评论

    本文标题:KMP算法理解

    本文链接:https://www.haomeiwen.com/subject/plitnttx.html