标签:包含 重要 img 组合 tps ast 模式匹配 char fast
KMP算法的核心思想为,当文本串与模式串在某一位置发生失配时,利用已经匹配部分的信息,让模式串迅速向后移动,以完成快速匹配。
模式串快速后移多少个单位,或者说失配后,文本串应该继续和模式串中的哪个字符继续比对,也就是常说的next[j]的值,这个值是只与模式串有关,而与文本串是无关的。
注意:下面讨论时,P[0,j)为左闭右开区间,包括下标为0不包括下标为j的字符,而P[0,j]则表示闭区间。
设文本串T和模式串S,假设在T[i]与P[j]处发生失配时,此时,我们已经掌握了文本串T[i-j,j)这部分子串的全部信息,KMP算法就是要利用这部分已知信息,快速确定T[i]应该继续和模式串中的哪个字符进行比对。
值得庆幸的是,这部分信息和P[0,j)是完全一致的。因为在比对T[i]与P[j]时,说明T[i-j,i)和P[0,j)已经是匹配成功的,才有继续往后比对的必要。
因此,我们可以根据模式串,提前计算出P[j]与文本串某个字符发生失配时,继续使用模式串哪个位置的字符与文本串进行比对,这个值就是next[j]。因此,KMP算法的核心就是提前计算出在模式串某位置j发生失配时,应该跳到哪个位置继续比对,这些位置组合起来,就是next数组。
注意:next[j]的值表示,当模式串P{j]与文本串T[i]发生失配时,使用P[next[j]]代替P[j]继续和T[i]进行下一次比对。
当T[i]与P[j]发生失配时,则利用next数组获得下一个应该比对的位置,继续和T[i]进行比对即可。
T[i]与P[j]失配时,拿到next[j],让P[next[j]]继续和T[i]进行比对,当然如果还是失配,那么继续和P[next[next[j]]]进行比对,一直这样迭代下去
按上述思路一直迭代下去,最终必然会出现两种情况:
2.1 j一直往前迭代,找到了某个位置j‘,使得P[j‘]=T[i],那么此时可以确定T[i-j‘, i]和P[0,j‘]已经匹配,二者一起后移,继续比对T[i+1]和P[j‘+1],这又是新一轮的比对
2.2 j一直往前迭代,都没有找到和T[i]一致的字符,此时,j‘一定会越界(程序中一般设置为-1)
,这种情况,说明找不到和T[i]对应的字符,应该跳过T[i],让T[i+1]和P[0]开始新一轮的比对
上述第二种情况,和T[i]与P[0]比对后不一致是类似的,此时,首字母不匹配,直接抛弃T[i],让T[i+1]和P[0]开始新一轮的比对
可以假想在模式串的下标为-1处有一个通配哨兵,这个哨兵与任何字符都是匹配的。当T[i]与P[0]失配时,继续往前,让T[i]与P[-1]进行比对,此时,必然比对成功,按照2.1的思路,即找到j‘=-1,这样,二者一起后移,继续比对T[i+1]和P[j‘+1],即T[i+1]和P[0],这样,便可以将2。2统一到和2.1一致的思路中
注意:利用哨兵时,next[0]必须为-1,这样,在越界时,使用2.1中的思路,找到的j‘是-1,,下轮比对才会恰巧是T[i+1]和P[0],因此,next[0]=-1也是next数组构造的初始条件
/*
KMP匹配
@param: T 文本串
@param: P 模式串
@return: 失败返回-1,成功则返回模式串在文本串的起始下标
*/
int kmpMatch(const char* T, const char* P){
int len = strlen(T);
int patternLen = strlen(P);
//生成next数组
int* next = new int[patternLen];
getNext(next, P);
int i = 0, j = 0;
//i<len,表示还能继续匹配
//j<patternLen,表示还没匹配成功
while(i < len && j < patternLen){
//j<0相当于匹配到通配哨兵
//T[i] == P[j]则表示当前比对通过
//这两种情况都为比对通过,文本串和模式串一起后移,继续比对T[i+1]和T[j‘+1]
if(j < 0 || T[i] == P[j]){
++i;
++j;
}else{
//按照2.2的思路,若找不到合适的j‘,则一直利用next数组往前迭代,直到找到相等的或者匹配到哨兵(哨兵是通配的)
j = next[j];
}
}
delete[] next;
//由于串长为patternLen,那么比对成功的起始位置不可能超过Len-patternLen
if(i-j > len-patternLen){
return -1;
}
return i-j;
}
再次强调,next数组的构造只和模式串有关
next[j]的值表示P[j]与T[i]失配后,使用P[nex[j]
继续和T[i]进行新一轮的比对
由于next数组的构造理解起来较为困难,因此先通过一个例子找一下规律:
设模式串P="aabbccaabbd",在下标为10的字母d处发生失配时,此时我们能掌握前缀P[0,10)信息,分析这个前缀,可以发现它的前缀"aabb"和它的后缀"aabb"是完全一致的,那么我们可以直接让P[4]替代P[10]继续下一轮的比对,在这个例子中,next[10]=4
仔细思考和分析,可以总结出这个规律:在P[j]处发生失配时,分析该位置之前的子串,找到它的最长公共前后缀,那么这部分公共前后缀的信息是可以不用比对的,可以让前缀的下一个字符替代P[j]和T[i]进行下一轮的比对
再注意一个微妙的地方:由于字符串的下标从0开始,因此最长公共前后缀的长度恰好就是下一个应该比对的字符的位置,因为长度正好等于最后一个字符位置+1
例如:前面的例子中,对于模式串P="aabbccaabbd",在P[10]处发生失配时,P[0,10)的最长公共前后缀为"aabb",其长度为4,而前缀"aabb"的最后一个字符b在字符串中的位置为3,那么跳过前缀,下一个比对的位置正好应该是3+1=4,其恰好也为字符串的长度4。
但是我们不可能对于模式串的每个位置,都直接去计算最长公前后缀,而是利用模式串自相似的特性,快速构造next数组
注意构造next数组的过程和KMP使用next的过程是类似的,构造模式串的过程就像是利用已经求的的前半部分的next数组,让前缀去匹配后缀,求出最新的next[j],一直这样往后迭代
因此,我们构造next数组都是基于已知的next[0,j],求next[j+1]的迭代过程:
此时P[j-j‘,j]相当于文本串
,那么,按照前面的思路,应该让j‘继续王钱迭代,知道找到P[j‘]=P[j],此时按照和上面相同的处理思路即可设模式串P="aabbccaabbd",len=11,下标为0-10,设变量i为已求出next值的最后一个下标,而j即为前缀P[0,i)的最长公共前后缀长度,也就是在P[i]处失配时替换的位置,即j=next[i]
初始化:i=0,j=-1,next[0]=-1,表示首字母失配时,和哨兵比对
注意:i为已经求出next值的最后一个位置,并利用next[0,i]的信息求解next[i+1],因此i<len-1. i+1最大为len-1。
其实1-3是递归求解可以匹配的j的过程,其过程和已知next数组进行KMP模式匹配的过程是一致的,只不错这里前缀充当模式串,后缀充当文本串
i=7,j=1,欲求next[8]:
其实next数组的迭代求解其实是利用了这样一个规律:当前仅当P[j]==P[next[j]]时, 这P[0,j+1]的最长公共前后前缀包含了P[0,j]的最长公共前后缀,且长度为P[0,j]的最长公共前后缀+1;若是不同,则说明P[0,j+1]的最长公共前后缀不包括P[0,j]的最长公共前后最,但可能包括P[0,next[j]]的最长公共前后缀,因此要一直往前找,知道找到相等的或者匹配到哨兵为止。
/*
构造next数组
*/
void getNext(int* next, const char* P){
next[0] = -1;
int len = strlen(P);
int i = 0;
int j = -1;
while(i < len-1){
//匹配到哨兵或者相等
if(j < 0 || P[i]==P[j]){
next[++i] = ++j;
}else{
//递归往前搜索
j = next[j];
}
}
}
上述的next数组的构造方式其实还是有可优化的空间的,我们来看一个极端的例子:
设模式串T="aa",则next[0]=-1,i=0,j=-1,判断P[0]==P[-1],是,那么next[++i]=++j,即next[1]=-1+1=0
问题就在于这个next[++i]=++j,next[1]=-1+1=0
首先,next[1]等于0表示,当P[1]与文本失配时,用P[0]替代P[1]继续和文本串进行比对,但毫无疑问,这次比对必然会失配,因为P[0]和P[1]是相等的,这样我们做了一次多余的比对后,让程序继续j=next[0]=-1;
由于我们在构造next数组时,求的最长公共前后缀的长度后,便直接将这个值作为下一个比对的位置(next[++i]=++j,这里的++j其实就是最长公共前后缀的长度)
,却不管这个位置上的字符是否已经和当前确定失配的字符是一样的,因此,才会多出来这些多余的比较
那前面的T="aabbccaabbd"来说,其原有的next数组为[-1,0,1,0,0,0,0,1,2,3,4]
,假设我们在第9个字符b处发生失配,求的P[0,8]的最长公共前后缀为3,我们便直接另next[9]=3,却不管P[3]是否和P[9]相等,而在这个例子中P[3]恰好等于P[9],P[9]失配则毫无疑问P[3]一定会失配,程序继续使用next[3]的值代替P[3]继续往前搜索。
但是,我们如果在构造next的过程中,提前判断P[next[j]]是否等于P[j],若相等,则跳过next[j]位置的比对,直接使用next[next[j]]处的值替代next[j],这样,便可以避免此次比对
如上述例子:j=9,原next[9]=3,由于P[3]==P[9],直接跳过P[3],使用next[3]代替3,这样便可以跳过很多不必要的比对
/*
构造next数组
*/
void getFastNext(int* next, const char* P){
next[0] = -1;
int len = strlen(P);
int i = 0;
int j = -1;
while(i < len-1){
if(j < 0 || P[i]==P[j]){
++i;
++j;
if(P[i]!=P[j]){
//若和当前已经失配的字符不相等,直接使用最长公共自前缀赋值
next[i] = j;
}else{
//若和当前已经失配的字符相等,则跳过本次比对,直接往前
next[i] = next[j];
}
}else{
j = next[j];
}
}
}
标签:包含 重要 img 组合 tps ast 模式匹配 char fast
原文地址:https://www.cnblogs.com/tommychok/p/9029082.html