对于两个字符串,在主串中s中查找是否存在某一个子串t(一个串中任意个连续字符组成的子序列(包含空串)称为该串的子串),如果存在则返回串t在串s中第一次出现的位置。串s为主串,子串t成为模式串,这个过程也称为模式匹配*,下面来详细介绍下串的两种模式匹配算法:BF算法和KMP算法
1.BF算法
BF算法也称暴力匹配算法或有回溯的匹配算法,具体步骤为:
- 从主串s的第一个字符开始与t的第一个字符做比较
- 如果相等,继续逐个比较后续的字符
- 如果不相等,使主串的第二个字符与t的第一个字符比较
- 如果相等,继续逐个比较后续的字符
- 如果不相等,使主串的第三个字符与t的第一个字符比较
- 不断循环上面步骤,直到匹配成功或匹配失败(即到主串末尾结束)
以一个例子来进行讲解:
(1) 首先串s的第一个字符’a’与串t的第一个字符’c’相比较
(2)两者不相等,则模式t向后滑动,使串s的第二个字符与串t的第一个字符比较
(3)两者不相等,模式t向后滑动,使串s的第三个字符与串t的第一个字符进行比较
(4)两者相等,继续逐个比较后续字符(就不再画图了)
(5)匹配成功,返回模式串t第一次在串s中出现的位置
设主串长度为m,模式串t长度为n,当主串前m-n个字符都是在比较到串t的最后一个字符匹配不成功时,模式串每个字符都比较了m-n次,总共的比较次数为(m-n)n,最后主串剩余的n个字符与模式串t的比较次数最多为n^2次,所以总的比较次数为最多为mn次,BF算法的复杂度为O(mn)
2.BF算法代码实现
int BF(char* s, char* t) {
if (s == NULL || t == NULL) {
PRSN; //打印串指针为空,为我们定义的一个宏
return NEQUAL; //NEQUAL表示不相等,为我们定义的一个宏
}
int i = 0, j = 0;
while (s[i] != '\0' && t[j] != '\0') {
if (s[i] == t[j]) {//两者相等,继续对比后续字符
i++;
j++;
}
else {
//主串从下一个位置开始
//因为j每次从0开始,因此j的值代表匹配过的长度
//所以此次比较主串开始字符的角标为i-j
//下次匹配时从下一个字符开始,所以+1
i = i - j + 1;
//模式串t从头开始
j = 0;
}
}
if (t[j] == '\0') {//如果j的值等于模式t的长度,则说明t已经完全匹配
return i - j;
}
else {
return NEQUAL;
}
}
3.KMP算法
KMP算法也称无回溯的模式匹配,在BF算法中,我们可以看到由于主串的i指针不停的回溯,导致算法的复杂度较高,当字符串较大时,将会耗费比较长的时间。于是KMP算法使主串的i指针不回溯,利用已经匹配的部分结果,使模式串尽可能向右滑动,减少匹配次数。
如在下图中,s[5]!=t[5],如果按照BF算法,i需要回溯到1,j需要回溯到0,然后再重新进行逐个比较,但是由匹配结果我们知道在j=5之前都有s[i]=t[j],又因为t[0]=t[3],t[1]=t[4],所以t[0]=s[3],t[1]=s[4],所以下次我们直接比较s[5]与t[2]即可,无需将i回溯,只需将j回溯到2即可
简单来说,KMP算法就是当匹配到某处不成功时,保持i不变,将j回溯到某个位置,因此我们修改一下BF的代码就得到了KMP的代码框架:
4.KMP算法代码框架
int KMP(char* s, char* t) {
if (s == NULL || t == NULL) {
PRSN;
return NEQUAL;
}
int i = 0, j = 0;
while (s[i] != '\0' && t[j] != '\0') {
if (s[i] == t[j]) {//两者相等,继续对比后续字符
i++;
j++;
}
else {
//i不变,让j改变
}
}
if (t[j] == '\0') {//如果j的值等于模式t的长度,则说明t已经完全匹配
return i - j;
}
else {
return NEQUAL; //NEQUAL表示不相等,为我们定义的一个宏
}
}
那么每次j该如何回溯呢?
5.next数组
在上面我们谈到,KMP利用之前匹配的结果,具体而言,如果对串t的某个字符t[j] (0≤j≤n-1),若存在一个整数k(1≤k<j),使得模式串t中k所指字符的前k个字符t[0],…,t[k-1]和t[j]前面的k个字符t[j-k],…,t[j-1]相同,并与主串s中i所指字符之前的k个字符相同,那么j下次可以直接回溯到k,i无需回溯
T[j-k] ~ T[j-1] = S[i-k] ~ S[i-1]
T[0] ~ T[j-1] = S[i-k] ~ S[i-1]
T[0] ~T[k-1] = T[j-k] ~ T[j-1]
K取何值时效率才最高呢?显然在满足上诉条件的前提下,K越大越好,即:
max{1≤k<j且T[0] ~T[k-1] = T[j-k] ~ T[j-1]}
用next数组来存储串t中每个位置j对应的k,next[j]=k,表示当S[i]!=T[j]时,j指针的下一个位置,next数组求法如下所示:
-
next[0]=-1,因为j已经在最左边了,如果这时候不匹配,只有将i向后移动
-
next[1]=0,这个很好理解,第二个位置匹配错误时,直接让j回退到0即可
-
j>1时,若t[k]=t[j]时,next[j+1]=k+1
为什么呢?因为T[0] ~ T[k-1] = T[j-k] ~ T[j-1],
如果T[k]=T[j],
那必然T[0] ~ T[k] = T[j-k] ~ T[j],
由图也可以看出,当绿色的部分相等时,如果T[k]=T[j],那蓝色的部分也必 然相等(即使两个绿色的部分有重合也是如此),
所以next[j+1]=k+1
-
j>1时,若t[k] != t[j]时,k=next[k]
当t[k] != t[j]时,显然上图中蓝色部分不再相等,这时我们无法求next[j+1],需要把k回退,k回退之前的位置为next[k],将k回退到next[k],之后再将t[k]与t[j]比较,如此往复直到t[k]==t[j]或者k回退到了-1,这时给next[j+1]赋值0即可,或者next[j+1]=k+1,这也是开始next[0]=-1而非0的好处(这其实是一种递归思想,读者可以好好体会下)
6.next数组获取的代码实现
void GetNext(char* t, int* next) {
if (t == NULL || next == NULL) {
printf("指针为空\n");
return;
}
next[0] = -1;
int len = strlen(t);
if (len == 1) {
return;
}
int j = 1;
int k = 0;
next[1] = 0;
while (j < len) {
if ((k == -1) || (t[k]==t[j])) {
next[++j] = ++k; //(1)
}
else {
k = next[k]; //(2)
}
}
}
可以看到代码并不复杂,但(1),(2)两句的逻辑需要读者仔细琢磨
7.KMP代码实现
有了next数组,我们再返回到上面的KMP代码,想要获取j的改变值通过next数组即可
int KMP(char* s, char* t, int next[]) {
if (s == NULL || t == NULL) {
PRSN;
return NEQUAL;
}
int i = 0;
int j = 0;
while (s[i] != '\0' && t[j] != '\0') {
if (j==-1 || s[i] == t[j]) {//两者相等,继续对比后续字符
i++;
j++;
}
else {
//i不变,让j改变
j = next[j];
}
}
if (t[j] == '\0') {//如果j的值等于模式t的长度,则说明t已经完全匹配
return i - j;
}
else {
return NEQUAL; //NEQUAL表示不相等,为我们定义的一个宏
}
}
8.KMP算法复杂度
设主串s长度为m,模式串t长度为n,获取next数组时由循环结构可知算法复杂度为O(n),在主串与模式串匹配过程中,因为i不回溯,所以复杂度为O(m),所以KMP算法复杂度为O(m+n),m远大于n时为O(m),因此当m,n均较大KMP算法效率是远高于BF算法的
完整代码:
链接:https://pan.baidu.com/s/15ZGdrDzaHZY9em6HecKI0Q
提取码:zfpp