KMP 算法简单解释
讲KMP算法,离不开BF,实际上,KMP就是BF升级版,主要流程和BF一样
不同是在匹配失败时能利用子串的特征减少回溯,利用根据子串特征生成的Next数组来减少
<( ̄︶ ̄)↗[GO!]
!!!所有数组下标都是从0开始
1. 先看看BF算法(暴力破解)#
int Brute_force_1(const char *S, const char *T) { if (!S || !T) return -1; int lenS = strlen(S); int lenT = strlen(T); int i = 0; //主串下标索引 int j = 0; //子串下标索引 while(i < lenS && j < lenT) { if (S[i] == T[j]) //如果相等一直继续往下匹配 ++i,++j; else //不相等i和j开始回溯 { i = i-j+1; j = 0; } } if (j == lenT) return i - j; return -1; }
BF算法有几种不同实现,但最终思想都是一样的,以下就是另一个BF实现
int Brute_force_2(const char *S, const char *T) { if (!S || !T) return -1; int lenS = strlen(S); int lenT = strlen(T); for (int i = 0; i <= lenS - lenT; ++i) { int k = i, j = 0; while (k < lenS && j < lenT && S[k] == T[j]) { ++j; ++k; } if (j == lenT) return i; //说明匹配到了 } return -1; }
你完全可以根据自己的理解写出BF算法,但在这里,为了BF和KMP统一,我们还是采用第一种实现,即容易看出回溯操作的实现
2. Next[]数组#
事实上,书上的next数组生成算法是经过优化后的算法,比较难懂,但你完全可以按照自己的理解做一个
注意:Next[]数组只是在KMP中字符串匹配失败时使用的
void GetNext(int Next[], char *str) { assert(str!=NULL); int len = strlen(str); if(len>1)Next[0]=0; //其实Next[0]等于0或者等于-1效果没什么影响, //因为在KMP中不匹配时判断是不是第一个字符不匹配用用的是j==0;-----if (j==0||Next[j]==0), if(len>2)Next[1]=0; //Next[]等于0时说明需要讲i回溯到子串头的下一个位置(i=i-j+1); //此时j也回到子串头位置(j=0) for(int i=2;i<len;++i) { for(int j=i-1;j>0;--j) { if(!strncmp(&str[0],&str[i-j],j)) { Next[i]=j;break; //找到最大重复子子串(子串中的子串) //Next[]为其他值则i不变,讲j回溯到Next[j]的位置(j=Next[j]) } else Next[i]=0; } } }
这个时间复杂度要比书上的方法高很多,但好理解,真实的反映了Next数组的本质。
3. KMP#
int KMP(const char *S, const char *T, const int *Next) { if (!S || !T||!Next) return -1; int lenS = strlen(S); int lenT = strlen(T); int i = 0; //主串下标索引 int j = 0; //子串下标索引 while(i < lenS && j < lenT) { if (S[i] == T[j]) ++i,++j; //若相等则继续匹配下一个字符 else //不相等则回溯 { //(当j==0时,即第一个字符不匹配,和Next[j]==0时事实上与BF算法相同) if (j==0||Next[j]==0) { i = i-j+1; j = 0; } else j = Next[j];//主串i位置不变,讲子串下标索引挪到Next[j]的位置 } } if (j == lenT) return i - j; return -1; }
这个回溯时的操作实际上是把两种情况合成一种,拆开后就是下面的,就是生成next数组那块三种情况
while (i < lenS && j < lenT) { if (S[i] == T[j]) ++i, ++j; else { if (j == 0) { ++i; //等价于i = i-0+1;j本身就等于0 } else if (Next[j] == 0) { i = i - j + 1; j = 0; } else { j = Next[j]; } } }
扩展#
Next数组有进一步改进的可能,如果发生失配,失配点子串字符若与回溯到的字符相同,则再次匹配肯定失败,所以改进的Next数组进一步处理了这种情况,消除了回溯
void GetNext_pro(int Next[], const char *str) { assert(str!=NULL); int len = strlen(str); if(len>1)Next[0]=-1; //其实Next[0]等于0或者等于-1效果没什么影响, //因为在KMP中不匹配时判断是不是第一个字符不匹配用用的是j==0;-----if (j==0||Next[j]==0), if(len>2)Next[1]=0; //Next[]等于0时说明需要讲i回溯到子串头的下一个位置(i=i-j+1); //此时j也回到子串头位置(j=0) for(int i=2;i<len;++i) { for(int j=i-1;j>0;--j) { if(!strncmp(&str[0],&str[i-j],j)) { if(str[i]==str[j]) Next[i]==Next[j]; else Next[i]=j; break; //找到最大重复子子串(子串中的子串) //Next[]为其他值则i不变,讲j回溯到Next[j]的位置(j=Next[j]) } else Next[i]=0; } } }
测试代码
int KMP(const char *S, const char *T) { if (!S || !T) return -1; int Next[MAXSIZE] = {0}; GetNext(Next,T); print_arr(Next, strlen(T)); GetNext_pro(Next,T); print_arr(Next, strlen(T)); int lenS = strlen(S); int lenT = strlen(T); int i = 0; //主串下标索引 int j = 0; //子串下标索引 while(i < lenS && j < lenT) { if (S[i] == T[j]) ++i,++j; //若相等则继续匹配下一个字符 else //不相等则回溯 { //(当j==0时,即第一个字符不匹配,和Next[j]==0时事实上与BF算法相同) if (j==0||Next[j]==0) { i = i-j+1; j = 0; } else j = Next[j];//主串i位置不变,将子串下标索引挪到Next[j]的位置 } } if (j == lenT) return i - j; return -1; } int main(void) { char source[MAXSIZE] = "adcfabadcf"; char target[MAXSIZE] = "abcabcabbac"; printf("%d\n", Brute_force_1(source, target)); printf("%d\n", Brute_force_2(source, target)); printf("%d\n", KMP(source, target)); getchar(); return 0; }
附上BF与KMP的比较,你会发现两者其实挺相似
总结#
其实核心就在于本文第一句话的理解。
KMP在子串含有相同前后缀时,利用Next数组减少匹配失败时的回溯次数有优势,而改进的Next数组在此基础上若子串含有较多相同字符则更进一步减少回溯。
所以KMP总之是利用子串的特征来削除回溯,如果子串并不具有这些特征,那就还没有BF好,因为KMP还需要额外的空间来存放Next数组
书上的next数组的生成很难懂,加油理解中。。。(ง •_•)ง
作者:chenyun
出处:https://www.cnblogs.com/starrys/p/11625840.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步