技术向程序员Java学习笔记

KMP算法理解

2017-03-12  本文已影响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>

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

按照暴力的方法 上面一串将回到第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>

/**

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>

/**

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>

/**

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>

/**

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;

}

/**

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

上一篇下一篇

猜你喜欢

热点阅读