KMP算法
算法用途
:用于两个字符串的匹配,称为字符串匹配。例如母串"ABCDEF"与子串"BCD",因为子串可以在母串中找到,所以他们是匹配上了的。
算法思想
利用子串的Next数组(下面会详细讲),在匹配时若匹配不上,无需从子串的头部开始,可以直接从Next[i]开始匹配,大大减小了原本暴力的做法。
Next数组
Next[i]表示在子串中(0~i-1)中的前缀和后缀的最长长度。
我们的Next[0] = -1是因为S1[0]前无元素,为了方便后续代码编写,特殊赋值为-1。
匹配过程
我们已经用眼睛求得了S1的Next数组,接下来开始匹配(以数组下标为0为基准),使用两个下标指针,用L遍历S1,R遍历S。
①当L = R = 3时发现S[R] != S1[L]
②令L = Next[L] = 1
③继续匹配,此时L = 1,R = 3,但发现S[R] != S1[L]
④令L = Next[L] = 0
⑤继续匹配,此时L = 0,R = 3,但发现S[R] != S1[L]
⑥令L = Next[L] = -1
⑦继续匹配,此时L = -1,R = 3,此时含义为S1的下标已经到达-1,意味S1[0] != S[R],此时直接令L++,R++
⑧继续匹配,此时L = 0,R = 4,发现S[R] == S1[L],直接L++,R++
⑨继续匹配至L = 3,R = 7时,发现S[R] != S[L],此时令L = Next[L] = 1
⑩继续匹配,此时发现S[R] = = S[L],继续R++,L++,直到最后我们匹配上了。
int KMP(char *S,char *S1) {
int l = 0,r = 0;
while(r<strlen(S)) {
if(l == -1||S1[l] == S[r]) {
l++;
r++;
}
else l = Next[l];
if(l==strlen(S1)) return r-l+1;
}
return -1;
}
接下来就是重头戏了,我们还需要求Next数组,才可以完成上述操作。
我们这里以子串为“a b c a b d a b c a b e a”为例
Next[i]表示回溯的位置,例如表一中的Next[3] = 1,因为aba中最长的相同前后缀为’a’与‘a’。我们在求他的时候,利用两个指针q与p,q初值附为-1,p附为0。我们发现,S1[0]前没有元素,那么就先让Next[0] = -1。
①当q == -1时,代表i~p-1内最长相同前后缀长度为0,故令Next[p] = q+1 = 0.
②当S1[q] = = S1[p]时,因为最长前后缀在增长,所以我们直接让Next[p] = Next[p-1] + 1.
③当S1[q] != S1[p]时,我们这时无需令q等于0去进行从头开始,比如上图,我们发现运行到此时,S1[0,q-1]一定是与S1[p-q,p-1]一定是相等的,因为不相等就运行不到这一步,那么我们又发现S1[0,Next[q]-1]与S1[q-Next[q],q-1]是一定相等的。那么S1[0,Next[q]-1]与S1[p-Next[q]-1,p-1]一定相等。所以我们此时令q = Next[q],这样就可以大大加快速度。
void ToNext(int *Next,char *S1) {
int q = -1,p = 0;
while(p<strlen(S1)) {
if(q == -1|| S1[q] == S1[p]) {
q++;
p++;
Next[p] = q;
}
else q = Next[q];
}
return ;
}
效率分析
我们在上述匹配过程中,发现当S[R] != S1[L]时,我们并非直接让L = 0从头开始匹配,而是让L = Next[L] ,之后再匹配S[R]与S1[L]。这么做是因为我们的Next[L]的含义——子串S1中0 ~ L-1中的前缀与后缀的最长长度,其实也等于了下标。那么我们直接让L = Next[L],此时子串中0~Next[L]-1中的元素一定与当前L-Next[L] ~ L-1中的元素对应相等,这样效率就极大的提高了。
时间复杂度分析
时间复杂度 = O(m+n),匹配时,最坏R从0移到m,过程中虽然L向前推了,但是S1相对于S还是向后移动,最坏匹配到最后一个元素,所以L也相对于S向后移动了m个长度。所以匹配时的复杂度是2 * m。
求Next数组时,我们也可以类比成S1与其自身的匹配,故复杂度为2 * n。所以总的时间复杂度为O(2n+2m)简写为O(n+m)。
写的很烂,有错请指出。