【注】致力于讲知识讲明白!不懂请留言!
定义
串的模式匹配是指子串的定位操作,也就是求子串(模式串)在主串中的位置;
1.暴力模式匹配(BF)算法
初始化两个数组下标,循环比较两个字符串的字符是否相等,相等则下标同时进一,比较下个元素直到匹配成功;不相等则使匹配的串下标回退到 0,被匹配的串下标回退到比较的初始位置+1,循环往复。
暴力模式匹配(BF)算法如下:
int index(SString S,SString T) //SString是存储字符串的结构
{
int i=1,j=1; //i 和 j 分别为主串和模式串的下标指示器
while(i <= S[0]&& j <= T[0]) //两个字符串比较的范围即不超过主串长度
//且j不超过模式串长度
{
if(S.ch[i] == T.ch[j]) //主串和模式串的对应赐福相同,则继续比较下一个字符长度
{
++i;
++j; //继续比较后面的字符
}
else
{
i=i-j+2; //如果主串和模式串发现字符不同,则需要回退
j=1;
}
}
if(j>T[0])
return i-T[0];//匹配结束后如果j大于模式串长度,说明整个模式串都匹配完毕
//也就是返回主串中匹配的初始位置,即减去模式串的长度
else
return 0; //没有匹配到,返回0
}
算法运行过程:(图中,i 和 j 表示匹配字符在数组中的位置下标):
【注】最坏的情况下的时间复杂度为O(nm);
举个例子分析下:假设n,m分别为主串和模式串的长度,最坏情况下,主串中的每一个位置,都要对模式串完整的比较一次,完整一次的比较运算次数为O(m),主串有n个位置,需要比较n轮,所以最坏情况下的时间复杂度O(n*m);
暴力模式匹配(BF)算法在进行模式匹配时,从主串的第一个字符开始,每次失败都是模式后移一位再从头开始比较,而某趟已经匹配相等的字符是模式的某个前缀,这种频繁的重复比较相当于模式串不断的进行自我比较,这就是其效率低的根源;
2.KMP算法
KMP算法是在BF算法的基础上改进得到的,利用已经部分匹配这个有效信息,保持 i 指针不回溯,通过修改 j 指针,让模式串尽量地移动到有效的位置,重点就在于当某一个字符与主串不匹配时,我们应该知道j指针要移动到哪里。即跳过了很多不需要比较的情况。如下图的分析:
第一种情况来说,在第一次比较失败的时候,之前的每次比较都是成功的,也就是说主串中这一部分和模式串的这一部分完全匹配的;
那既然前面的部分是完全匹配,那么暴力算法的接下来几次匹配肯定是成立的,也就是说这些比较其实是不需要的;
另外一种情况就是,在第一次比较失败的时候,之前的每次比较都是不成功的,也就是说主串中这一部分和模式串的这一部分不完全匹配的;
模式串前面都匹配的abcd部分由于a后面是bcd,这三个字符显然和a不匹配,所以暴力算法接下来的每次都后移一个位置显然是都不可能匹配的;
所以在这种情况下,我们前面这几次比较就是需要跳过的,那我们该一次性跳过几个字符呢?能否利用第一次得到的信息来预知后面移动的位置呢?
我们可以通过next数组(即记录模式串中每个位置发生匹配失败时,下一次和主串对应位置对比的字符的下标)来解决这个问题,那如何来求next数组呢? 下面先看一下进行匹配的过程:
在这个图中,第一行是主串,第二行是我们需要匹配的模式串(在竖线之前是完全匹配的),最后一行是在发生不匹配时,模式串需要进行后移(t 之前是不需要匹配的,即P[1, t] = P[j-t, j]),然后就是模式串中 t 位置重新和主串中 i 的比较,不过需要注意的是我们的这个t可能不至一个,如下图:
在这个图里面呢,我们可以取第一个 a 和最后一个 a,当然也可以取前面的 aba 和后面的 aba,但是我们的next数组一定是一个固定的值,这里我们需要选择的值是最大的 t,就是 j - t 最小,即移动距离最小的,(从安全角度考虑的),根据下面的图进行理解:
根据上面的分析,我们大概就理解了next数组,下面看一下求next数组的代码
void get_next(char T[], int next[]){ //基于递推思想
i = 1;
next[1] = 0;
j = 0;
while(i <= T[0]){
if(j == 0 || T[i] == T[j])
++i;
++j;
next[j] = i;
}
else
j = next[j];
}
看到这个代码呢,肯定是一脸懵逼的(大佬除外),下面根据一个例子来分析下代码:
先看两个特殊的情况:
next[j] 的值表示当S[i] != T[j] 时,j 指针(模式串指针)的下一步移动位置;
如果第一个位置就不匹配了,指针 j 已经无法再左移到之前匹配的位置,所以解决办法就是右移指针 i (主串指针);
如果第二个位置就不匹配了,指针 j 只能右移到第一个位置;
然后再考虑一般的情况1:
如果6号位置不匹配了,那根据KMP算法,此时前后缀匹配最长为 ab,长度为2,所以指针 j 左移到2+1=3号位置,故next[6] = 3;
如果7号位置不匹配了,那根据KMP算法,此时前后缀匹配最长为 abc,长度为3,所以指针 j 左移到3+1=4号位置,故next[7] = 4;
这就是递推的一大类情况,那它在代码中的体现就是:
if(j == 0 || T[i] == T[j]) //j==0先不考虑
++i;
++j;
next[j] = i;
再考虑一般的情况2:
这时候 i 所指的 b 和 j 所指的 c 不相等,所以我们前后缀相同的最大长度不可能为4(abax),只能寻找更短一点的相同前后缀(比如ab) ;
如下图:
向前循环找更短一点的相同前后缀
那它在代码中的体现就是:
else
j = next[j];
最后再单独分析下 j == 0的情况:
T[2] = b != T[1] = a 说明前后缀不等,进入 else语句:
else
j = next[j];
如果不考虑这种情况,那么循环到这里就走不下去了,因为T[0]不存在或者说这时主串没有字符,更不会有所谓的前后缀了;我们知道当
j = 0 时,next值直接为1(因为前后缀相同的子串长度为0,所以移动到0+1=1的位置,所以next值就为1),所以可以正好并入到 if 的这种情况中;
经过当面的分析,我们再重新看一下求next数组的代码:
void get_next(char T[], int next[]){ //基于递推思想
i = 1;
next[1] = 0; //第一个字符不匹配,next值设为0
j = 0;
while(i <= T[0]){ //递推字符串各个位置
if(j == 0 || T[i] == T[j])
++i;
++j;
next[j] = i; //如果i和j位置元素相同,j加1后再赋值给next的下一个位置
}
else
j = next[j];//如果i和j位置元素不同,则去寻找更短的相同前后缀
}
知道next数组的求法后,KMP算法就比较容易理解了,下面看一下KMP算法的代码:
int KMP(char S[], char T[], int next[])
{
int i=1,j=1; //i 和 j 分别为主串和模式串的下标指示器
while(i <= S[0]&& j <= T[0]) //两个字符串比较的范围即不超过主串长度且j不超过模式串长度
{
if(j == 0 || T[i] == T[j])
{
++i;
++j; //继续比较后面的字符
}
else
j = next[j];
}
if(j>T[0])
return i-T[0];//匹配结束后如果j大于模式串长度,说明整个模式串都匹配完毕
//也就是返回主串中匹配的初始位置,即减去模式串的长度
else
return 0; //没有匹配到,返回0
}
最后进行KMP算法的效率分析:
根据上面的代码,我们可以设变量K = 2i - j;
每次循环迭代,如果时满足 if 的情况, i 的值加1,j 的值加 1,K 的值也会加1;
如果时满足else的情况,由于next[j]的值肯定比 j 小,所以 j 至少减去1,那么K的值至少增加1。
总之,无论那种情况,K的值都是随着算法的不断迭代,总是不断增大的;
算法开始,i = 1, j = 1,所以K初值为1;
算法结束时,i 至多遍历到主串末尾,即 i 最大为主串长n,j 最小是0;所以K = 2i - j <= O(n),KMP算法还需要得到next数组,计算next数组的方式和KMP算法是类似的过程,所以不难计算next数组的时间复杂度,O(m)(m是模式串的长度),所以KMP算法的时间复杂度O(m+n)
【注】相对于暴力算法直接降为线性的了!