KMP算法

来源:互联网 发布:资源三号卫星数据下载 编辑:程序博客网 时间:2024/06/18 02:24

算法名称的由来:KMP算法其实就是有三个人提出了一种关于字符串匹配的算法,并且这三个人的名字都比较难记,所以干脆每个人取一个字母,于是就有了KMP算法。

算法解决问题:传统的字符串匹配算法是主串和子串一位一位的比较,当比较到某一位不相等的时候就将主串回退到这次开始比较的字符的下一个字符,子串回退到首字符重新进行比较,而KMP算法则是解决了主串回退的问题,每次比较如果发现不一致,不需要回退主串,而只需要回退子串就可以了。

算法原理:假设有主串ababcababcabababc和子串ababcabababc,从头开始比较,发现比较到第十个字符的时候不一致,那么如果是传统的算法就需要从主串的第二个字符和子串的第一个字符进行比较。但是KMP算法则认为不能够从主串的第二个开始,这样太浪费时间了。应该是主串不动,子串向前回退到第五个字符,因为主串的第十个字符前面的几个字符为abab,这个和子串的前四个字符是一样的,那么就不需要比较了,如果第十个字符和子串的第五个字符一样的话就可以继续往下比较,而不用回退了。也就是说到不一致的时候就需要观察,主串中该不一致字符前面有多少个字符和子串的开始几个字符一致,如果能找到这样的字符,那么只需要子串移动到相应位置即可。就像例子中主串的第十个字符c前面可以找到和子串的ababcXXXXX对应的就是abab,所以只要将子串的c和主串的该字符比较即可。

下面进行一般性分析:当匹配到主串S的j处和子串P的k处(之前已经匹配成功),说明在主串中j前面的k-1个字符和子串中的前k-1个字符是一样的。也就是

S(1)...S(j-k+1)......S(j-1)S(j)...S(m)

           P(1).............P(k-1)P(k)...P(n)

上述两个式子中下划线部分是一样的,下面在进行比对的时候发现S(j)!=P(k)那么现在就要找到子串的回退位置,也就是要找到一个长为i的串,使得S(j)前面的i-1个字符和子串的前i-1个字符一致,这样就可以比较S(j)和P(i)是否一致了,也就是这i个字符就不需要重新比较了,而且主串也不需要回退了。那么也就是说:

S(1)...S(j-k+1)...S(j-i+1)...S(j-1)S(j)...S(m)

           P(1)......P(i-1).......P(k-1)P(k)...P(n)

要找到上面的有下划线部分一致的最大的i,又因为主串中的S(j-i+1)~S(j-1)和子串中的P(k-i+1)~P(k-1)一致,所以只要在P中找到P(1)~P(i-1)=P(k-i+1)~P(k-1)的最大的i就可以直接移动到i位置进行比较了。通俗的讲就是当匹配到不一致的时候就要找子串当前位置之前的字符串中最长的前缀和后缀,并且要求前缀和后缀一致。所以最终需要计算的移动的位置都是针对子串的,而与主串没有关系。

算法核心:该算法的核心就是找到每次比较不一致的时候子串需要回退的位置,这样比较的时候就可以直接回退了。

算法中的数据结构:next数组,用于存放子串中对应的回退位置。

对于上述例子中的子串ababcabababc进行计算,

        首先第一个字符,因为第一个字符的前面没有字符,所以其最长一致前后缀就是0,next[0]=0,也就是找不到一个前缀和一个后缀是一致的子串;

        对于第二个字符,第二个字符前面就只有一个字符,所以没有最长一致前后缀,所以next[1]=1,也就是如果不一致,要回退到第一个字符;

        对于第三个字符,其前面有ab,找不到最长一致前后缀,所以应该置next[2]=1,也就是遇到这个字符不一致,应该回退到第一个字符去;

        对于第四个字符,其前面有aba,其最长一致前后缀为a,也就是说next[3]=2,也就是遇到这个字符不一致,应该回退到第二个字符;

        对于第五个字符,其前面有abab,其最长一致前后缀为ab,那么next[4]=3,也就是遇到这个字符不一致,就回退到第三个字符;

        对于第六个字符,其前面有ababc,找不到最长一致前后缀,那么next[5]=1,也就是遇到这个字符不一致,就回退到第一个字符;

        对于第七个字符,其前面有ababca,其最长一致前后缀为a,那么next[6]=2,也就是遇到这个字符不一致,就回退到第二个字符;

        对于第八个字符,其前面有ababcab,其最长一致前后缀为ab,那么next[7]=3,也就是遇到这个字符不一致,就回退到第三个字符;

        对于第九个字符,其前面有ababcaba,其最长一致前后缀为aba,那么next[8]=4,也就是遇到这个字符不一致,就回退到第四个字符;

        对于第十个字符,其前面有ababcabab,其最长一致前后缀为abab,那么next[9]=5,也就是遇到这个字符不一致,就回退到第五个字符;

        对于第十一个字符,其前面有ababcababa,其最长一致前后缀为aba,那么next[10]=4,也就是遇到这个字符不一致,就回退到第四个字符;

        对于第十二个字符,其前面有ababcababab,其最长一致前后缀为abab,那么next[11]=5,也就是遇到这个字符不一致,就回退到第五个字符;

上面是根据字符串手工计算的,那么在算法里面可以根据如下规则计算:

首先第一个字符的next值为0也就是next[0]=0,第二个字符的next值为1也就是next[1]=1,下面假设next[j]=k,也就是说第j个字符的next值为k,那么根据前面的分析就可以得到

P(1)~P(k-1)=P(j-k+1)~P(j-1)。下面要计算next[j+1]的值,根据子串中P(j)是否等于P(k)分为两种情况

①、若两者相等那么P(1)~P(k-1)P(k)=P(j-k+1)~P(j-1)P(j),那么就会有next[j+1]=k+1

②、若两者不相等,那么就需要移动子串,因为如果是主串和子串匹配的时候发现该位置不一致,那么就需要将子串移动到上一个可能和主串一致的地方,也就是要将k设置为next[k],也就是找到前一个相同的地方,然后查看P(j)是否等于P(k),一直比较直到相等或者到第一个字符,比较到第一个字符的时候就将P(j)置为1。

具体来说就是如下过程:

        首先第一个字符,next[0]=0;

        对于第二个字符,next[1]=1;

        对于第三个字符,其前面的字符为b,next值为1,但是b不等于第一个字符a,所以继续找a对应的字符,a的next值为0,所以本位置的next值为1;

        对于第四个字符,其前面的字符为a,next值为1,而且a等于第一个字符a,所以本位置的next值为1+1=2;

        对于第五个字符,其前面的字符为b,next值为2,而且b等于第二个字符b,所以本位置的next值为2+1=3;

        对于第六个字符,其前面的字符为c,next值为3,但是c不等于第三个字符a,所以继续找a对应的字符,a的next值为1,查找发现第一个字符为a,和本位置的c不一致,然后查找第一个字符的next值,为0,所以本位置的next值为1;

        对于第七个字符,其前面的字符为a,next值为1,而且a等于第一个字符a,所以本位置的next值为1+1=2;

        对于第八个字符,其前面的字符为b,next值为2,而且b等于第二个字符b,所以本位置的next值为2+1=3;

        对于第九个字符,其前面的字符为a,next值为3,而且a等于第三个字符a,所以本位置的next值为3+1=4;

        对于第十个字符,其前面的字符为b,next值为4,而且b等于第四个字符b,所以本位置的next值为4+1=5;

        对于第十一个字符,其前面的字符为a,next值为5,但是a不等于第五个字符c,所以继续找c对应的字符,c的next值为3,查找发现第三个字符为a,和本位置的a一致,所以本位置的next值为c的next值加一,也就是3+1=4;

        对于第十二个字符,其前面的字符为b,next值为4,而且b等于第四个字符b,所以本位置的next值为4+1=5;

具体的代码实现如下:

public int[] kmp(int[] arr) {int i = 1;int[] next = new int[arr.length];next[0] = 0;next[1] = 1;int k = next[i];while (i < arr.length - 1) {if (k == 0 || arr[i] == arr[k - 1]) {next[i + 1] = k + 1;k += 1;i++;} else {k = next[k - 1];}}                return next; }

上述代码中的next数组就是最终的结果数组。之所以上述代码中有next[k-1和arr[k-1]是因为k在next数组中表示的是第k个字符,并且是从1开始计数的。

KMP算法中next数组的优化

在主串和子串进行比较的时候,到达某一位发现不匹配,那么如果只是回退到next数组指示的位置上的话,并不一定能够满足条件,因为根据next数组回退的那个位置上的字符可能和刚刚进行匹配的字符是一样的,那么可以肯定的是还是不匹配。所以这里可以进行简化,在求得的next数组中,如果发现某一位置的字符和其next值对应的字符是一样的那么就可以讲next值直接赋值为next值对应字符的next值。具体例子就是说对于上述的ababcabababc子串以及其next数组011231234545从第三个字符开始,第三个字符和第一个字符(next[2]=1)是一样的,所以可以将第三个字符的next值设为和第一个字符的next值一样,也就是0。以此类推,最终的next数组可以转换为010130101513,这样在比较到第六个字符的时候如果不一致就知道需要回退到第零个字符处,也就是从头开始。

具体代码如下:

for (int j = 2; j < next.length; j++) {if (arr[j] == arr[next[j] - 1]) {next[j] = next[next[j] - 1];}}


以上就是KMP算法的主要过程,主要参考了严蔚敏老师的讲课视频,地址为点击打开链接



原创粉丝点击