字符串匹配算法:简单来说就是给你一个主串和一个子串,让你查找子串在主串中的位置,找到返回下标。
常见的两种算法:BF算法、KMP算法
这两种算法是怎样的思路呢,我们接着往下看:
目录
BF算法(暴力算法)
BF算法是普通模式匹配算法,其思路就是将子串的第一个字符与主串的第一个字符进行匹配,如果相等,则继续比较两串对应的第二个字符;如果不相等,则比较子串的第二个字符和主串的第一个字符,依次比较下去,直到得出最后的匹配结果。
我们通过例子来理解一下这段话的含义:
假设给定一个主串为:a b a b c a b c d a b c d e
子串为:a b c d
我们的规则是在主串中找到子串,则返回主串中的第一次匹配到的下标,失败则返回-1.
那么我们按照暴力算法的思路是,给定两个变量i、j用来记录两个字符串的下标,首先让i、j分别指向两字符串的下标为0的位置,然后开始比较,如果对应字符相同,那么两个下标同时向后挪动一位,我们观察到例子中前两个字符都对应相等,当下标走到第三个字符的位置时,即i=2,j=2时,对应字符不相等,那么我们接下来的操作是,让i指向第二个字符,j回归到下标为0的位置,重新开始比较,按照此思路,直到在主串找到子串,即为成功。我们再通过画图理解一下:
这张图中呢我提到匹配到不同字符之后i返回i-j+1的位置是怎么的出来的呢,其实很简单昂,因为你的i和j是同时向后移动的,所以返回的时候移动的距离就是j的值,再加1,i就指向下一个位置啦。
看最后一步,当主串中可以找到子串时,这时候j可以遍历到结束,所以我们可以根据这个特点找到主串中匹配到的第一个字符的下标。这里我们需要考虑一种情况,这种情况我在图中没有显现出来,就是如果主串中找不到子串会发生什么呢,很容易想到哈,就是i遍历到尾,所以我们可以根据这个把匹配不到子串的这种情况考虑进来。那么分析到这里呢,我们暴力算法的思路就理清了,下面我们把这个例子对应的代码写一下:
//str代表主串
//sub代表子串
//返回值:返回字串在主串当中的下标,如果不存在则返回-1
int BF(char* str, char* sub)
{
assert(str && sub);
if (str == NULL || sub == NULL)
{
return -1;
}
int lenStr = strlen(str);
int lenSub = strlen(sub);
int i = 0,j = 0;
while (i < lenStr && j < lenSub)
{
if (str[i] == sub[j])
{
i++;
j++;
}
else
{
i = i - j + 1;
j = 0;
}
}
if (j >= lenSub)
return i - j;
return -1;
}
容易发现,如果我们主串的长度为M,子串长度为N,那么这个代码的时间复杂度就是O(M*N)我们可以感受到BF算法其实并不高效,所以我们就引出了KMP算法。
KMP算法
KMP算法是一种改进的字符串匹配算法,其核心是利用匹配失败后的信息,尽量减少主串和子串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next函数实现,函数本身包含了子串的局部匹配信息。相比于暴力算法,它的时间复杂度为O(M+N)
区别:KMP和BF唯一不一样的地方是,主串的i不会回退,并且j也不会移动到0号位置。
那么怎么理解上面的表述呢,我们依然是通过例子来理解。
KMP算法理解
1.为什么主串不回退?
如图这两组字符串,假设现在i,j指向2号位置,匹配失败,那么按照暴力算法回退到1号位置,也是没有必要的,1号位置的字符b和子串0号位置的a也不一样。
2.解释j的回退位置
如图第二阶段时,匹配失败了,我们不回退i是因为在这个地方匹配失败,说明i和j之前,是有一部分相同的,因为如果不同,两下标不可能走到这。
那么继续,我们看第三阶段的图,我特殊标记了a b段,这两组字符串呢比较有特点就是在这,因为我们发现画横线的三个a b段其实是相同的,什么意思呢,意思是我的j可以从下标为2的位置开始往后读,因为前面的字符跟主串很吻合。
我提这些什么目的呢,跟开始一样,我要让i不回退,j回退到特定位置。
ok,通过例子,我们现在的问题就成了怎么确定j回退的位置?
我们依然还是看上边的那个例子,注意我们假设子串为p,通过分析我们可以知道p[0]--p[1]这段字符串跟p[3]--p[4]这段是相同的,这两段的串长度为2,j回退的位置也刚好是2。回退之后我们要比较的就是c和a了,此时不相同,因此j还要回退,到这思路有点了,我们这里先保留思路,再整理一下:
我们的i本身是不回退的,所以我们要尽量在 主串中找到与子串匹配的一部分串。假设我们找到了相匹配的那部分串,那么我们的j就回退到那部分串的下一个位置,那么到这我们就可以引出下一个概念了:next数组,这个next数组是什么呢,当我们保存子串某个位置匹配失败后,j回退的位置,比如例子中的5下标回退的位置是2,所以其实主串的每一个下标都会在next数组中对应一个位置,而这个对应关系需要你通过一个函数求得。
3.next数组
现在正式给出next数组:也就是用next[j]=k来表示,不同的j对应一个k,k就是你要将j移动的位置。
k的求法:
1>.规则:找到匹配成功部分的两个相同的真子串(不包括自身),一个以下标0开始,另一个以j-1下标结尾。
2>.不管数据next[0]=-1,next[1]=0。
我们还是先用上边的例子简单理解一下,比如刚刚得出next[5]=2,此时j=2,2号位置对应的字符是c,我们根据上边的规则看一下,一个以下标为0开始也就是a,另一个以j-1也就是b结尾,也就是要在c字符前找到以a开始,以b结束的两个字符串,很明显,c之前只有一个ab,不存在两个,所以长度为0,所以这里的k=0,也就是next[2]=0,同样的道理我们就可以得出上边的例子所对应的next数组元素为:-1,0,0,0,1,2
下面我们再练习两个充分理解一下next数组:
这里呢我就不对这两组进行分析了,还请读者自行推算,不理解可以在评论区说出来,本人看到一定解决。
ok到这呢相信大家对next数组已经了解了,那么我们接下来分析一下:
已知next[i]=k,怎么求next[i+1]?
如图,由于需要结合图,所以我直接将解释部分包含在了图中,这里可以得出呢,
当p[i]=p[k]时,next[i+1]=k+1;
那么当p[i]!=p[k]时,next[i+1]=?
我们依然以图片的形式解答:
代码实现
到此为止,思路已经整理出来了,接下来就是代码实现了
#include<stdio.h>
#include<assert.h>
#include<string.h>
//str代表主串
//sub代表子串
//pos代表从主串的pos位置开始找
int KMP(char* str, char* sub, int pos)
{
assert(str != NULL && sub != NULL);
int lenStr = strlen(str);
int lenSub = strlen(sub);
if (lenStr == 0 || lenSub == 0)
return -1;
if (pos < 0 || pos >= lenStr)
return -1;
int i = pos;//遍历主串
int j = 0;//遍历子串
while (i < lenStr && j < lenSub)
{
if (j==-1||str[i] == sub[j])//相同下标都后移
{
i++;
j++;
}
else//不同则j回退到next[j]位置
{
j = next[j];
}
}
if (j >= lenSub)
{
return i - j;
}
return -1;
}
GetNext函数
写到这呢我们代码大体写出来了,需要完善的地方就是如何得到next数组,需要另外写一个GetNext函数
void GetNext(char* sub,int* next,int lenSub)
{
next[0] = -1;
next[1] = 0;
int i = 2;//注意这里的i是当前的下标,相较于之前图中的i是加1的,意思是现在的i相当于图中的i+1
int k = 0;//前一项的k
while (i < lenSub)
{
if (k==-1||sub[i - 1] == sub[k])
{
next[i] = k + 1;
i++;
k++;
}
else
{
k = next[k];
}
}
}
至此KMP算法就基本结束了,下面附上所有代码:
KMP算法完整代码实现
#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<stdlib.h>
//str代表主串
//sub代表子串
//pos代表从主串的pos位置开始找
void GetNext(char* sub,int* next,int lenSub)
{
next[0] = -1;
next[1] = 0;
int i = 2;//注意这里的i是当前的下标,相较于之前图中的i是加1的,意思是现在的i相当于图中的i+1
int k = 0;//前一项的k
while (i < lenSub)
{
if (k==-1||sub[i - 1] == sub[k])
{
next[i] = k + 1;
i++;
k++;
}
else
{
k = next[k];
}
}
}
int KMP(char* str, char* sub, int pos)
{
assert(str != NULL && sub != NULL);
int lenStr = strlen(str);
int lenSub = strlen(sub);
if (lenStr == 0 || lenSub == 0)
return -1;
if (pos < 0 || pos >= lenStr)
return -1;
int* next = (int*)malloc(sizeof(int) * lenSub);
assert(next);
GetNext(sub,next,lenSub);
int i = pos;//遍历主串
int j = 0;//遍历子串
while (i < lenStr && j < lenSub)
{
if (j==-1||str[i] == sub[j])//相同下标都后移
{
i++;
j++;
}
else//不同则j回退到next[j]位置
{
j = next[j];
}
}
if (j >= lenSub)
{
return i - j;
}
return -1;
}
next数组优化
接下来呢,我们对next数组进行优化----nextval数组
如果回退后的位置和当前字符一样,就写回退到那个位置的nextval值;如果回退到的位置和当前字符不一样,就写当前字符原来的next值。
结合图我们来理解一下,比如如果我们i指向下标为5的位置发现匹配失败,那么按照next数组需要回退到下标为4的位置,但是下标为4的位置对应的字符跟当前字符一样,所以仍然需要继续回退,所以我们的nextval就是直接回退到最后一步,实现优化,如果i指向下标为8的位置匹配失败,回退时发现回退的字符没有一样的,那么就写当前字符原来的next值就可以了。
ok我们再来一个例子理解一下:
这个例题我就不做解释了,有问题的欢迎在评论区留言。
优化后的代码我就不做演示了,我给个思路:我们在回退时先判断一下,如果回退后的下标对应的字符与原位置相同,则继续回退,直到不相同为止。感兴趣的小伙伴可以代码实现一下。
以上呢就是KMP算法的全部内容了(内容参考自哔哩哔哩up主:比特大博哥),有所收获的老铁三连,我们下一篇不见不散~