简单介绍一下什么是next数组,毕竟网上有很多懂意思就行,本文的重点在代码解释与三个简单面试例题上。next数组就是记录str中每个位置前缀与后缀的匹配长度(不包含该位置自身)。例如:str="ababac",那么它的next数组就是[-1,0,0,1,2,3];第3个字符‘b’的前缀是‘a’,后缀也是‘a’,长度为1,则next[3]=1。最后一个字符‘c’的前缀就是‘aba’,后缀肯定和前缀相同也是‘aba’,长度为3,next[5]=3。前两位是人为规定的,分别为-1与0。在实际应用的时候,根据情况要多求一位next值,比如此时next[6]就等于0,待会下面例题有应用。
如何求next数组?你可以暴力求解,每个等长的前缀与后缀都进行比较,当然时间复杂度肯定很高O(n^2)。
这里简单介绍一下O(n)的方法。精炼下来就是:
第一个和第二个位置分别人为规定为-1,0。
第i(i>1)个位置的next值求解方式为:将(i-1)字符前缀的下一个字符与(i-1)位置的字符比较,相同的话next值较前一位+1;不等的话,递归查询已经部分生成的next数组,cn=next[cn],这里cn就是上次前缀的位置,直到字符相同或者cn变为0、-1为止。cn变为0、-1的时候,该位置的next值就为0。cn有两个含义:该位置1、前缀的长度 2、next值。
求next数组的具体代码如下:
int *NextArray(int next[],char str2[])
{
next[0]=-1;//默认-1
next[1]=0;//默认0
int cn=0;//前缀的长度(前缀下一个字符的下标)以及记录next数组的值
int i=2;//str2 位标从2开始
while(i<strlen(str2))//next数组正常与str2长度相同,但根据不同题目有的要多求一位
{
if(str2[i-1]==str2[cn]){
next[i++]=++cn;//相同的话next值较前一位+1
}
else if(cn>0){
cn=next[cn];//不等的话,递归查询已经部分生成的next数组,确定较小前缀长度
}else{
next[i++]=0;//cn变为0、-1的时候,该位置的next值就为0
}
}
return next;
}
应用实例1:KMP
有一个文本串str1,和一个模式串str2,现在要查找str2在str1中首次出现的位置。
暴力方法,依次挨个匹配,那么最差时间复杂度为O(m*n)。例如 aaaaaaab 与 aab
代码如下:
int main()
{
char a[]="aaaaaaab";
char b[]="aab";
int na=strlen(a);
int nb=strlen(b);
for(int i=0;i<na;i++){
int k=i;
bool flag=true;
for(int j=0;j<nb;j++){
if(a[k]==b[j])
{
k++;
}
else{
flag=false;
break;
}
}
if(flag){
cout<<i;
break;
}
}
}
KMP方法是首先也是挨个匹配,相同的时候,i++,j++。(i为str1的计位器,j为str2的计位器);出现不同值的时候,如果此时匹配的是str2[0],那str1的下标计数器i直接++,继续与str2[0]比较。如果不是str2[0],那么j=next[j],再继续比较str[i]与str[j]。
代码如下:
//得到str1中首次与str2相同的第一个下标位置
int getIndex(char str1[],char str2[])
{
int *next=new int[strlen(str2)];//new一个next数组,长度与str2相同
NextArray(next,str2);//获得next数组
int i=0;//str1下标
int j=0;//str2下标
while(i<strlen(str1)&&j<strlen(str2))
{//两结束条件1、str1到末尾都没有匹配成功 2、str2走至末尾说明成功
if(str1[i]==str2[j]){
i++;j++;
}else if (next[j]==-1) {
//等于-1的时候说明i位置以及之前没有任何与str2有能匹配的,next数组的首位,此时j必等于0
//i++与str2重新开始从头匹配比较
i++;
}else {
j=next[j];
}
}
return i-j;
}
应用实例2
已知一个原始串str1,在其后面添加字符,生成str2,使其变成2个原始串且每个串的开头不同,要求添加的字符越少越好,求str2。例如:原始串str1为aaba,那么str2为aabaaba,添加aba,生成了两个原始串。str1=abcabc,那么str2=abcabcabc。
思路:为了添加的字符最少,即要求公共子序列的部分要尽可能大。公共部分即为原串末位再后一位的最长前缀。这里提及到本文开始提到的多求一位next值。str1=aaba,其next数组为[-1,0,1,0,1],多求了一位。next[4]=1,那么说明可以复用的最长前缀长度为1,原字符串去掉第0个位置的剩下的即为要添加的部分‘’aba‘’。str1=abcabc,next数组为[-1,0,0,0,1,2,3],next[6]=3,则原字符串前三位为公共部分,将剩余部分添加。
代码如下:
next数组多求一位即将int *NextArray(int next[],char str2[])中while循环条件加个=号,其余不变,代码改为:
while(i<=strlen(str2))
char *getnewstr(char str1[])
{
int *next=new int[strlen(str1)+1];//生成的next数组长度+1
NextArray(next,str1);
int l=next[strlen(str1)];//l为末位前缀、最长公共子序列
char *str2=new char[2*strlen(str1)-l];
for(int i=0;i<strlen(str1);i++)
{
str2[i]=str1[i];
}
for(int i=strlen(str1);i<strlen(str2);i++)
{
str2[i]=str1[l];
l++;
}
return str2;
}
判断一个字符串是否由一个子串重复获得,即str1=n*str2.例如:abcabcabc,就是由子串abc重复得到。
思路:本题同样先求n+1位的next数组。例如abcabcabc,其next数组为[-1,0,0,0,1,2,3,4,5,6],可以发现9-next[9]=3。再例如aabaaabaaaba,由aaba重复得到,其next数组为[-1,0,1,0,1,2,2,3,4,5,6,7,8],可以发现12-next[12]=4。即length-next[length]的结果如果能被length整除,则含有重复子串。
这是为什么呢?
如果字符串是由某个子串n倍重复构成的,那么其末位的前缀必然是n-1个子串的长度,用总长度减去n-1个子串的长度就是单个子串的长度。此长度必然能够被总长度整除。
如果字符串不是由某个子串n倍重复构成的,假设其有子串,其末位的前缀长度必定小于一个子串的长度,用总长度减去末位的前缀,这个长度肯定远大于一个子串的长度,即使整个字符串是由两个字符串组成的,也还是大于一个子串的长度。此时用总长度除以这个长度,肯定不能整除。
代码如下:
char *getstr(char str1[])
{
int *next=new int[strlen(str1)+1];
NextArray(next,str1);
int l=strlen(str1)-next[strlen(str1)];
if(strlen(str1)%l==0) {
char *str2=new char[l];
for(int i=0;i<l;i++)
{
str2[i]=str1[i];
}
return str2;
}else {
return 0;
}
}