问题描述
在用KMP算法实现该问题前先说明以下什么是KMP算法,以及KMP算法的常规实现是怎样的。
KMP算法:
首先先看一组例子,上面为主串,下面为模式串
由图中可以看到,模式串的前五个字符和主串的前五个字符是相同的,即匹配的,而第六个开始,主串与模式串的字符开始不同。那么这个时候,开始看模式串前五位中的最大公共前缀是啥,如下图所示:
这个时候我们可以发现,模式串中最长的可以与主串相匹配的子串为GTG,因此接下来,我们可以移动模式串,将模式串中匹配的最大前缀子串向右移动,移到和主串中的三个相互匹配的最长可匹配后缀子串同一个位置。
然后继续比较公共前后缀之后的字符,发现A和T依旧不匹配,按照上一步的思想,应该继续在GTG三个字符中找最长公共前后缀,此时,只有G字符满足条件,因此将模式串中的第一个G字符向右移动,移到和主串中的第三个字符G相同的位置。
此时可以看到,匹配好的模式串的前方已经没有前缀了,主串和模式串的首位已经对其,现在的情况和开始的时候就是一样的了,接下来继续按照相同的方法对模式串进行移动匹配即可。
但是,在实际的计算机代码中是无法实现抽象的模式串移动的,那么,我们该如何间接的表示这种移动呢?
在KMP算法中,采用了一个next[ ]数组,来存放模式串中每个字符前对应的最大公共前缀的长度。一般情况下,模式串的前两位的next数组值一般设置为-1,0或者0,1;为了方便理解,在这里设置为-1和0;
例如:字符串 "abcabcaaa"的next[ ]数组的值为[-1,0,0,0,1,2,3,4,0]
具体的手工算法可以参考一下的一个b站视频:
https://www.bilibili.com/video/BV12J411m74v
在代码中通常设置一个大小不超过模式串长度的辅助位置指针变量k来间接的代替移动的功能。当需要移动的时候,k就指向next[k]的位置。
以上就是KMP算法的基本思想,也是最常规的一种情况。
接下来按照C语言与Java来分别实现一下KMP的代码
C语言:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef struct
{
char *str;
int length;
int maxlength;
}DString;
void getnext(DString T,int next[]);
int KMP(DString S,DString T,int next[]);
void Initiate(DString *S,int max,char *string);
void main()
{
//定义主串
DString S;
//定义子串
DString T;
int next[100];
int index;
//以下是初始化操作
Initiate(&S,100,"foiahfohaofihqoifhakfjoqwifjfasofiqhfklafhqhfoiqfhaf");
Initiate(&T,5,"wifjf");
getnext(T,next);
index=KMP(S,T,next);
printf("%d \n",index);
system("pause");
}
//S为主串,T为模式串
void getnext(DString T,int next[])
{
int j=1,k=0;
next[0]=-1;
next[1]=0;
while(j<=T.length-1)
{
if(T.str[j]==T.str[k])
{
next[j+1]=k+1;
j++;
k++;
}
else if(k==0)
{
next[j+1]=0;
j++;
}
else k=next[k];
}
}
int KMP(DString S,DString T,int next[])
{
//定义辅助变量i遍历主串,辅助变量k遍历字串
//由于辅助变量k对应了next数组里的值,因此可以将k的值简单的理解为位置
//再通过位置来反向理解与继续接下来的操作
int i=0,k=0,v;
while(i<S.length&&k<T.length)
{
//k=-1表示指向子串的指针停留在首位置,所以接下来的操作就是将两个指向主串和子串的指针向后移动,进行位移
if(k==-1||S.str[i]==T.str[k])
{
i++;
k++;
}
//如果两个指针对应位置的值不相等,则将k指针返回到最大公共前缀的位置,最差的结果就是再次返回到起始位置
else k=next[k];
//当k指针的位置超过了子串的长度的时候,就将匹配到的位置下标返回,否则返回-1表示没有找到
}
if(k==T.length)
v=i-T.length;
else v=-1;
return v;
}
void Initiate(DString *S,int max,char *string)
{
int i;
S->str= (char *)malloc(sizeof(char)*max);
S->maxlength=max;
S->length=strlen(string);
for(i=0;i<S->length;i++){
S->str[i]=string[i];
}
}
运行结果:
Java语言:
class Solution {
public boolean repeatedSubstringPattern(String s) {
}
public int KMP(int start,String S,String T,List<Integer> list){
int i=start,k=0;
while(i<S.length()&&k<T.length()){
if(k==-1||S.charAt(i)==T.charAt(k)){
i++;
k++;
}
else k=list.get(k);
}
if(k==T.length()-1){
return i-T.length();
}
else return -1;
}
public List<Integer> getnext(String pattern){
List<Integer> list =new ArrayList<>();
int j=1,k=0;
list.add(-1);
list.add(0);
while(j<pattern.length()-1)
{
if(pattern.charAt(j)==pattern.charAt(k)){
list.add(k+1);
j++;
k++;
}
else if(k==0){
list.add(0);
j++;
}
else k=list.get(k);
}
return list;
}
}
以上就是KMP算法的基本思想与代码实现,至少课本上的最基本的原理是这样的。
下面就是今天的这道题目所运用的KMP算法的实际提现,其中的next[ ]数组与课本上所讲的前两位设置为-1,0又有些许的区别,且最后面的一位设置为0或者是-1(-1的出现和一开始的next数组初始化有关)。
现在开始正式分析这题:
问题分析:该题给我们的函数传递了一个字符串,让我们判断该字符串是否是由一组其对应的子串重复连接组成的,是则返回布尔值true,否则返回布尔值false。
我们假定该字符串是s,实际上,当一个字符串是由其一个子串重复连接实现的时候,s字符串就可以看作是字符串 s+s(s在s+s中并非从首位置开始,也就是并不是简单的看作是在s的后方直接连接了一个s字符串)的一个子串。
所以就可以采用KMP算法,在s+s中,从非首位置开始,把s字符串作为模式串(可以理解为”返主为客“),在s+s字符串中查找,如果找到了,那么就说明s字符串是由其一个子串相继连接组成的。
关于这种思想的正确性的证明就不展开论述了,过程也比较复杂,就简单的在下方放一个数学证明过程的图,有兴趣的朋友们可以自己研究一下:
那么接下来就是用java来实现该问题的解决方案,上代码!
这是Leetcode上的官方解法:
class Solution {
public boolean repeatedSubstringPattern(String s) {
return kmp(s + s, s);
}
public boolean kmp(String query, String pattern) {
int n = query.length();
int m = pattern.length();
int[] fail = new int[m];
Arrays.fill(fail, -1);
for (int i = 1; i < m; ++i) {
int j = fail[i - 1];
while (j != -1 && pattern.charAt(j + 1) != pattern.charAt(i)) {
j = fail[j];
}
if (pattern.charAt(j + 1) == pattern.charAt(i)) {
fail[i] = j + 1;
}
}
int match = -1;
for (int i = 1; i < n - 1; ++i) {
while (match != -1 && pattern.charAt(match + 1) != query.charAt(i)) {
match = fail[match];
}
if (pattern.charAt(match + 1) == query.charAt(i)) {
++match;
if (match == m - 1) {
return true;
}
}
}
return false;
}
}
本人的代码:
class Solution {
public boolean repeatedSubstringPattern(String s) {
List<Integer> list =new ArrayList<>();
list=getnext(s);
int v=KMP(1,s+s,s,list);
if(v!=-1&&v!=s.length()){
return true;
}
return false;
}
public int KMP(int start,String S,String T,List<Integer> list){
int i=start,k=0;
while(i<S.length()&&k<T.length()){
if(k==-1||S.charAt(i)==T.charAt(k)){
i++;
k++;
}
else k=list.get(k);
}
if(k==T.length()){
return i-T.length();
}
else return -1;
}
public List<Integer> getnext(String pattern){
List<Integer> list =new ArrayList<>();
int j=1,k=0;
list.add(-1);
list.add(0);
while(j<pattern.length()-1)
{
if(pattern.charAt(j)==pattern.charAt(k)){
list.add(k+1);
j++;
k++;
}
else if(k==0){
list.add(0);
j++;
}
else k=list.get(k);
}
return list;
}
}
当然,这道题可以直接用String类的方法indexof来解决,代码在下方,本文只讨论kmp算法在具体题目中的应用。
class Solution {
public boolean repeatedSubstringPattern(String s) {
//在s+s字符串中从1位置开始搜寻s字符串,搜寻到返回第一次出现的位置所在的索引值
return (s + s).indexOf(s, 1) != s.length();
}
}