数据结构–KMP算法
首先我在这里提出以下问题,一会一起进行探讨
1.什么是最长公共前后缀
2. KMP算法怎么实现对匹配原理
3. 最长公共前后缀怎么求解
KMP算法可以用来解决什么问题?
答:在字符串中匹配子串,也称为模式匹配
分析
什么是最长公共前后缀?
通俗来说,就是一个字符串开头到某一个位置与其某一个位置到字符串结束一模一样,即 P0…PK-1 与 Pi-k…Pi-1的字符串相同,注意我们这里的最长公共前后缀是指的真串,即不包含该字母,如对于字符串 aba,其前缀有 a,ab,其后缀有ba,a那么最长公共公共前后缀为a
下面是实例:
对于ababcabababe
我们手动遍历一次从 i = 0开始
i = 0,字符串为 a
因为不包含本身,所以其无前后缀,那么自然也没有公共前后缀
i = 1,字符串为 ab
其 前缀为 a,后缀为 b,无公共前后缀
i = 2,字符串为 aba
其 前缀有 a,ab 后缀有 ba,a 公共前后缀为a
i = 3,字符串为 abab
其前缀为 a,ab,aba 其后缀有bab,ab,b最长公共前后缀为ab
i = 4,字符串为 ababc
其前缀为 a,ab,aba,abab 其后缀有babc,abc,bc,c 所以无公共前后缀
同理,下面的我们肉眼观察(开头有没有一段字符串和结尾一段字符串相同)
i = 5,字符串为 ababca
其最长公共前后缀为 a
i = 6,字符串为 ababcab
其最长公共前后缀为 ab
i = 7,字符串为 ababcaba
其最长公共前后缀为 aba
i = 8,字符串为 ababcabab
其最长公共前后缀为 abab
i = 9,字符串为 ababcababa
其最长公共前后缀为 aba
i = 10,字符串为 ababcababab
其最长公共前后缀为 abab
i = 11,字符串为 ababcabababe
其无公共前后缀
对于 i 遍历到每个元素,我们可以创建一个Next[ ]数组来保存该元素之前的所有字符形成的字符串的最长公共前后缀
那么对于任意字符串,第一个元素没有前面的元素,我们则把其的Next置位-1,即Next[ 0 ] = -1;而对于第一个元素,我们知道只有一个元素时,其永远没有不包含本身的前后缀,所以第二个元素保存的该元素之前的所有字符形成的字符串的最长公共前后缀永远为0,即永远Next[ 1 ] = 0;
对于这样保存的方法,我们有下图
那么问题是怎么求解Next[ ] 数组
Next[ ]数组求解(最长公共前后缀怎么求解)
完成从上面的理论分析到代码实现,我们需要两个"指针",因为最长公共前后缀本质上前面的一段字符串等于最后的一段字符串,那么,我们用 i 来完成对后面字符串的遍历,而 用 k 来完成对前面字符串的遍历(看不懂别急,下面还有解释)
代码:
public void getNext(int [] Next,String s){int i = 0;// 遍历字符串的后端int k = -1;// 遍历字符串的前端Next[0] = -1;// 初始第一个为-1while (i < s.length()){if (k == -1 || s.charAt(i) == s.charAt(k)){// 如果前面没有公共前后缀时,k为-1,那么其下一个索引元素储存的就为++k(就为0)// 如果前面的字符串有最长公共前后缀,且在该元素(后面字符串的最后一位)等于前面字符串(存储的前面字符串的公共前缀)的下一位时// 那么下一个索引元素(++i)储存的就为++kNext[++i] = ++k;}else {// 如果该元素(后面字符串的最后一位)不等于前面字符串(存储的前面字符串的公共前缀)的下一位// 令 k = Next[k]得到其前面的字符串的公共前缀的下一位(不懂可以看图)k = Next[k];}}}
对于字符串ababcabababe
这里举几个特殊的例子来进行原理说明:
i 为遍历后缀的索引,k为遍历前缀的索引
这里约定:i 指向的位置为黄色,k指向的位置为绿色
1.当该元素的前面的字符串具有公共前后缀时,当这个元素和前面字符串的公共前缀的下一位相等,那么就会形成在原来长度基础上+1的新的最长公共前后缀
当字符串遍历到b时,此时字符串为ababcabab
如上图,此时在对k = 3(绿色),i = 8(黄色)位置进行判断,此时完成上面最后蓝色位置的判断后,黄色b应该保存前面字符串的最长的公共前后缀3,然后此时i 指向黄色位置,k指向绿色位置,我们发现只要黄色和绿色位置的元素相等,他们就可以在前面的基础上形成一个新的最长公共前后缀,长度加1,为4,存储在如图橘色位置,如此 那么如果
s.charAt(i) == s.charAt(k)
且其前面的字符串有公共前后缀,则下个元素存储的Next数组中的值会在前面的值的基础上加1
Next[++i] = ++k;
2.当这个元素和前面字符串的公共前缀的下一位不相等时
我们接着上面的情况继续遍历下去,上面判断完i(黄),k(绿)色的字符后,i 增加到下图黄色位置,k增加到下图绿色位置,并把长度4(如图所示的蓝色公共前后缀)存储在其Next[ ]数组里,此时k = 4,i = 9 ,字符串为ababcababa
此时判断在 k = 4 和 i = 9 的位置的字符是否相等,不相等的话我们令 k = Next[ k ];
k = Next[k];
然后重复的判断,k = Next[ k ] 时 与 i = 9 时的字符是否相等
原理解释,因为上图的绿色与黄色是对应关系,在他们之前的字符串,上图蓝色是相同的,而我此时读取上图绿色位置的Next数组的值,就可以知道上图绿色前面的字符串有几位字符串长度的公共前后缀,在这个例子当中我们可以知道是两个,即下图的蓝色和紫色是相同的,因为绿色和黄色位置前面字符串是相同的,所以黄色前的字符串也有类似的性质
那么我们知道绿色前面的字符串的蓝色与紫色相等,而黄色前面的字符串和绿色前面的字符串相同,且其蓝色也等于紫色,那么 蓝1(第一部分蓝色) = 紫1(第一部分紫色),紫1 = 紫2(第二部分紫色),可知 蓝1 = 紫2,这样我们就直接知道了其除去上图蓝色外的最长的公共前后缀
在这个基础上我们只用完成下图 k = 2(绿) 和 i = 9(黄)的位置的字符判断,而减少判断其前面的蓝色部分的字符串部分,如果k = 2(绿) 和 i = 9(黄)的位置的字符不相等的话,重复上述操作,k = Next[ k ],得k = Next[ 2 ] = 0,然后重复上面的操作,最后把最长公共前后缀的长度存储在下图橘色位置
KMP算法怎么实现对匹配原理
我们仍然举例子来介绍这种匹配方式,如下有:
主串S:ababcabcacbab,模式串T:abcac
传统的BF解法是 在主串 ababcabcacbab 的每一个元素开始把子串匹配,直到匹配成功,这种匹配方式需要 把 主串的 指针 i 回溯
BF 代码
int BF(char S[],char T[])
{int i=0,j=0;while(S[i]!='\0'&&T[j]!='\0'){if(S[i]==T[j]){i++;j++;}else{i=i-j+1;j=0;}}if(T[j]=='\0') return (i-j); //主串中存在该模式返回下标号 else return -1; //主串中不存在该模式
}
BF的流程
约定:i绿色指向主串,k黄色指向子串
第一次匹配(从 i = 0 开始)
匹配到 i = 2 和 j = 2 时发现不匹配,i 退回到 i = 1 ,从 i = 1 ,j = 0开始重新匹配
第三次匹配(从 i = 2 开始匹配)
发现 i = 6 和 j = 4 不匹配,蓝色是起始位置,下次从 i = 3,j = 0 开始匹配
上面就是BF算法的实现,我们发现每次匹配失败后,i都要回退,这样需要消耗大量时间,而如果我们可以读取子串该匹配元素的前面字符串的最长公共前缀(橘色),那么看图一,图中蓝色位置的字符串相等,而根据其公共前后缀相等,看图二,其中蓝色部分相等,那么我们的i就可以不回退,而令j = 该位置Next[ ]数组存储的元素,再进行判断,看图三,我们知道此时i 仍然等于 6,j = 1,此时进行遍历
而其前面字符串如果没有公共前缀,那么j = 0,就是从子串头开始遍历
图一:
图二:
图三: