一、定义
在主字符串(主串)中查找子字符串(子串),如果能够找到子串,则返回子串的首字符的位置。
如果未找到,则返回-1。
二、实现字符串匹配的两种算法
(一)、BF算法(暴力求解)
1.理解
对于子串中的每个字符,都在主串中遍历,如果两者字符相同,则子串和主串都向后移动,如果不相同,则子串中的字符回退到子串的首字符,主串中的字符回退到这一趟失配开始位置的下一个位置。
①循环同时遍历主串和子串。如果有一个遍历到结尾则退出循环。
②判断主串和子串的字符是否相等,如果相等,则主串和子串均向后移动,如果不相等,则让子串回到首字符的位置,让主串回到这一趟失配开始位置的下一个位置。
③对循环退出的条件判断是否能够找到子串的位置。
如果子串能够越界,则表示找到了子串的位置。
2.画图理解
3.代码实现
(1)从主串首字符开始匹配
int BF_Matching(const char str[],const char sub[])
{
//1.循环遍历两个字符串
//①求两个字符串的长度
int len_str=strlen(str);//不计算'\0'
int len_sub=strlen(sub);
//②定义两个变量遍历字符串
int i,j=0;
//③循环遍历
while(i<len_str&&j<len_sub)
{
//2.判断是否相同
//①相同,都向后移动
if(str[i]==sub[j])
{
i++;
j++;
}
//②不同
else
{
i=i-j+1;//主串回到失配开始位置的下一个位置
j=0;//子串回到首字符位置
}
}
//3.判断退出的条件
if(j<len_sub)
{
return -1;
}
return i-j;
}
(2)从主串特定位置开始匹配
int BF_Matching_Pos(const char str[],const char sub[],int pos)
{
// 1.获取两个字符串的长度
int len_str=strlen(str);
int len_sub=strlen(sub);
//2.循环同时遍历两个字符串
int i=pos;//主串从特定位置开始
int j=0;
while(i<len_str&&j<len_sub)
{
//判断
if(str[i]==sub[j]) // 相同,则继续
{
i++;
j++;
}
else //不同
{
i=i-j+1;//i回退到失配开始位置的下一个位置
j=0;//j回退到首字符位置
}
}
//判断循环退出的条件
if(j<len_sub)
{
return -1;
}
return i-j;//适配位置
}
(二)、KMP算法(对BF算法的优化)
1.理解
BF算法通过不断让主串回退到开始失配位置的下一个位置,而KMP算法则不让主串回退。
分两种情况:
(1)子串的首字符在后续的子串字符中未出现:此时主串不需要回退,回退也会发生失配,无意义。
(2)子串的首字符在后续的子串字符中出现:此时主串也不需要回退,虽然回退有意义,但此时在失配位置之前的字符串与主串仍然相等,所以通过让子串回退到合适位置,从而代替主串的回退。
结论:此两种情况覆盖了所有可能性,即主串可以不回退,只是让子串回退到合适位置。
2.画图理解
(1)子串的首字符在后续子串字符中未出现
(2)子串的首字符在后续的子串字符中出现
3.寻找子串的回退位置
由上图可以看出,子串的回退位置其实就是左红或者上紫的长度。
(1)next数组存放回退位置:
由于子串的每一个位置都可能发生失配的现象(需要回退),针对此种存放多个数据的情况,可以用next数组存放每个失配位置的回退位置。
(2) next数组赋值方法:
①前两个字符赋值-1和0。
②求下一个字符的next值:
如果当前已知位置的字符和回退位置的字符相同,则下一个字符next=当前回退位置+1。
如果当前已知位置的字符和回退位置字符不相同,则让其不断回退比较,直到回退到-1。此时下一个字符next=0。
(3)nextval数组:是对next数组的优化
如果当前位置的字符和回退位置的字符相同,且当前位置字符与主串的字符匹配不上,那么回退位置的字符与主串也肯定匹配不上,所以回退无价值。
(4)nextval数组的赋值方法:
①前一个字符赋值为-1。
②根据当前字符的next求当前字符的nextval:
如果当前位置的字符与其回退位置(next)的字符相同,则当前位置nextval=回退位置nextval。
如果当前位置的字符与其回退位置(next)的字符不相同,则当前位置nextval=当前位置的next。
注意:如果要求nextval数组,要先知道next数组。
4.代码实现
(1)通过next数组实现
int* Get_Next_Val(const char sub[],int len)
{
//1.分配空间:数组分配无法确定数组的下标
//采用动态申请内存 不知道预先数组的长度,所以动态申请较好
int* next=(int*)malloc(sizeof(int)*len);
if(next==NULL)
exit(1);
//2.实现next数组:
//前两个位置分别赋值-1,0
next[0]=-1;
next[1]=0;
int k=next[1] ;//k表示当前的回退位置,k不停在变化
//通过已知位置得出下一个位置
int i=1;
while(i<len-1)
{
//回退位置 k //回退位置字符 sub[k]
//如果当前已知字符与回退位置字符相同,则下一个位置的回退位置+1
//如果回退到-1,表示触底了,则下一个位置的回退位置=当前回退位置+1
if(k==-1||sub[i]==sub[k])
{
i++;//下一个位置
k++; //回退位置+1
next[i]=k;
}
//如果当前已知字符与回退位置字符不相同,则继续回退
else
{
k=next[k];
}
}
return next;
}
int KMP_Pos(const char str[],const char sub[],int pos)
{
//1.循环遍历两个字符串
//①求两个字符串的长度
int len_str=strlen(str);//不计算'\0'
int len_sub=strlen(sub);
//②定义两个变量遍历字符串
int i=pos;
int j=0;
//③循环遍历
//定义next数组
int* next=Get_Next_Val(sub,len_sub);
while(i<len_str&&j<len_sub)
{
//2.判断是否相同
//①相同,都向后移动
//j如果回退到-1,表示子串的字符与主串不相同,则主串和子串均要移动
if(j==-1||str[i]==sub[j])
{
i++;
j++;
}
//②不同,i不回退,j回退到合适位置 (j可能回退到-1)
else
{
j=next[j];
}
}
//3.判断退出的条件
if(j>=len_sub)
{
return i-j;
}
free(next);
return -1;
}
(2)通过nextval数组实现
int* Get_Excellent_Next(const char sub[],int len)
{
int* next=Get_Next_Val(sub,len); //获取next数组
//1.分配空间:数组分配无法确定数组的下标
//采用动态申请内存 不知道预先数组的长度,所以申请较好
int* nextval=(int*)malloc(sizeof(int)*len);
if(nextval==NULL)
exit(1);
//2.实现nextval数组:
//前一个位置赋值-1
nextval[0]=-1;
//通过已知位置得出下一个位置
for(int i=1;i<len;i++)
{
//如果当前已知字符与回退位置字符相同,则nextval=回退位置nextval
if(sub[i]==sub[next[i]])
{
nextval[i]=nextval[next[i]];
}
//如果当前已知字符与回退位置字符不相同,则nextval=回退位置的next
else
{
nextval[i]=next[i];
}
}
free(next);
return nextval;
}
int KMP_Pos_Nextval(const char str[],const char sub[],int pos)
{
//1.循环遍历两个字符串
//①求两个字符串的长度
int len_str=strlen(str);//不计算'\0'
int len_sub=strlen(sub);
//②定义两个变量遍历字符串
int i=pos;
int j=0;
//③循环遍历
//获取nextval数组
int* nextval=Get_Excellent_Next(sub,len_sub);
while(i<len_str&&j<len_sub)
{
//2.判断是否相同
//①相同,都向后移动
//j如果回退到-1,表示子串的字符与主串不相同,则主串和子串均要移动
if(j==-1||str[i]==sub[j])
{
i++;
j++;
}
//②不同,i不回退,j回退到合适位置 (j可能回退到-1)
else
{
j=nextval[j];
}
}
//3.判断退出的条件
if(j>=len_sub)
{
return i-j;
}
free(nextval);
return -1;
}