字符串匹配算法——KMP算法学习
2013-11-12 20:57
295 查看
KMP算法是用来解决字符串的匹配问题的,即在字符串S中寻找字符串P。形式定义:假设存在长度为n的字符数组S[0...n-1],长度为m的字符数组P[0...m-1],是否存在i,使得SiSi+1...Si+m-1等于P0P1...Pm-1,若存在,则匹配成功,若不存在则匹配失败。该问题经常出现在编辑器中,即常用的find或ctrl-F命令,所以字符串匹配算法的复杂度直接影响编辑器的效率。
首先考虑朴素字符串匹配的方法。其思想是:循环以字符数组S中的每一个字符作为起点,与字符数组P进行匹配。其代码如下所示:
上面代码只返回首次匹配成功时,字符数组S的起点下标。在遍历数组S时,做了一步小的优化,即起点只能出现在[0...n-m]里。
假设进行下面的匹配:
当Si与Pj不匹配,即Si≠Pj,此时根据上面的算法,S将把起点“回溯”至Si-j+1,P将向前“滑动”一位,即下次将是Si-j+1与P0进行比较。
可以看到上面算法的复杂度为O(n*m),其在每次匹配失败时,都将S的起点进行回溯,从而重新匹配。而KMP算法的思想是:在匹配失败时,不回溯S而只滑动P,来降低算法复杂度。
再次考虑上面的情况,当Si与Pj不匹配,即Si≠Pj时:
若P0P1...Pj-2≠P1P2...Pj-1时,则朴素匹配的下一步,S将把起点“回溯”至Si-j+1,P将向前“滑动”一位,可直接跳过
若P0P1...Pj-3≠P2P3...Pj-1时,则朴素匹配的下下一步,S将把起点“回溯”至Si-j+2,P将向前“滑动”两位,也可直接跳过
直到P0P1...Pk-1=Pj-kPj-k+1...Pj-1时,S无需回溯,直接将P向前滑动j-k位,即Si与Pk进行比较,这便是KMP算法的核心思想。
为了算法方便,可引入next[]数组来记录满足P0P1...Pk-1=Pj-kPj-k+1...Pj-1的k值
k保证最大,可确保P滑动位数j-k最小,从而确保不会移动过多,错过匹配。
假设已知next[]数组,KMP算法如下代码所示:
接下来,问题将转换为如何求next[]数组。
方法一:直接根据上述定义来求,即对于每一个j,使K从j-1到1依次遍历,若满足P0P1...Pk-1=Pj-kPj-k+1...Pj-1,则break,并记录k值,具体代码如下:
方法二:将next[]数组的求解问题转换为KMP字符串匹配问题,然后使用递归的方式求解
假设已知next[j]=k,求next[k+1],其计算过程如下图所示
因为next[j]=k,所以P0P1...Pk-1=Pj-kPj-k+1...Pj-1
若Pk=Pj,则P0P1...Pk-1Pk=Pj-kPj-k+1...Pj-1Pj,所以next[j+1]=k+1
若Pk≠Pj,则该问题可类比于KMP字符串匹配问题,上图中第一行相当于字符串S,第二行相当于字符串P,此时S不回溯,只对P向前滑动,即滑动到Pnext[k]与Pj来进行比较,所以可递归的令k=next[k],直到Pk=Pj时,next[j+1]=k+1
将上述思想转换为代码如下:
至此,KMP算法的完整思想学习完毕。
KMP算法中next[]数组的其它应用:参考HDU 1358
题意:字符串S,若其某个前缀满足Ak,即前缀有k个字符串A连接而成,则输出前缀的长度和k。若某个前缀可有多个满足,则只输出最大的k
解决:假设A的长度为i,若长度为j的前缀满足Ak,即P0P1...Pi-1PiPi+1...P2i-1......P(k-1)iP(k-1)i+1...Pki-1Pj,此时j=k*i,根据上面的定义,可以知道next[j]=(k-1)*i,所以字符串A的长度i=j-next[j],k=j/i,且j%i==0
如何证明此时的循环次数k为最大?使用反证法即可,若有更大的k,再推导出已知不成立
所以本题的代码如下:
首先考虑朴素字符串匹配的方法。其思想是:循环以字符数组S中的每一个字符作为起点,与字符数组P进行匹配。其代码如下所示:
int naiveStrMatch(char* s, char* p) { int i, j; int n = strlen(s), m = strlen(p); for(i=0; i<(n-m+1); i++) { for(j=0; j<m&&s[i+j]==p[j]; j++); if(j == m) return i; } return -1; }
上面代码只返回首次匹配成功时,字符数组S的起点下标。在遍历数组S时,做了一步小的优化,即起点只能出现在[0...n-m]里。
假设进行下面的匹配:
S0 | S1 | ... | Si-j | Si-j+1 | ... | Si-1 | Si | ... | Sn-1 |
P0 | P1 | Pj-1 | Pj |
可以看到上面算法的复杂度为O(n*m),其在每次匹配失败时,都将S的起点进行回溯,从而重新匹配。而KMP算法的思想是:在匹配失败时,不回溯S而只滑动P,来降低算法复杂度。
再次考虑上面的情况,当Si与Pj不匹配,即Si≠Pj时:
若P0P1...Pj-2≠P1P2...Pj-1时,则朴素匹配的下一步,S将把起点“回溯”至Si-j+1,P将向前“滑动”一位,可直接跳过
若P0P1...Pj-3≠P2P3...Pj-1时,则朴素匹配的下下一步,S将把起点“回溯”至Si-j+2,P将向前“滑动”两位,也可直接跳过
直到P0P1...Pk-1=Pj-kPj-k+1...Pj-1时,S无需回溯,直接将P向前滑动j-k位,即Si与Pk进行比较,这便是KMP算法的核心思想。
为了算法方便,可引入next[]数组来记录满足P0P1...Pk-1=Pj-kPj-k+1...Pj-1的k值
k保证最大,可确保P滑动位数j-k最小,从而确保不会移动过多,错过匹配。
假设已知next[]数组,KMP算法如下代码所示:
int KMPStrMatch(char* s, char* p, int* next) { int i, j; int n = strlen(s), m = strlen(p); /*for循环保证S不回溯*/ for(i=0, j=0; i<n; i++) { /*当s[i]!=p[j]时,只滑动p至p[next[j]]*/ while(j>=0 && s[i]!=p[j]) j = next[j]; /*j++表示比较下一位*/ if(j==-1 || s[i]==p[j]) j++; /*返回匹配成功的起点*/ if(j == m) return i-m+1; } return -1; }
接下来,问题将转换为如何求next[]数组。
方法一:直接根据上述定义来求,即对于每一个j,使K从j-1到1依次遍历,若满足P0P1...Pk-1=Pj-kPj-k+1...Pj-1,则break,并记录k值,具体代码如下:
void getNext1(char* p, int* next) { int i, j, k; int m = strlen(p); next[0] = -1; for(j=1; j<m; j++) { for(k=j-1; k>0; k--) { for(i=0; i<k&&p[i]==p[j-k+i]; i++); if(i == k) break; } next[j] = k; } }
方法二:将next[]数组的求解问题转换为KMP字符串匹配问题,然后使用递归的方式求解
假设已知next[j]=k,求next[k+1],其计算过程如下图所示
P0 | P1 | ... | Pj-k | Pj-k+1 | ... | Pj-1 | Pj | Pj+1 |
P0 | P1 | ... | Pk-1 | Pk |
若Pk=Pj,则P0P1...Pk-1Pk=Pj-kPj-k+1...Pj-1Pj,所以next[j+1]=k+1
若Pk≠Pj,则该问题可类比于KMP字符串匹配问题,上图中第一行相当于字符串S,第二行相当于字符串P,此时S不回溯,只对P向前滑动,即滑动到Pnext[k]与Pj来进行比较,所以可递归的令k=next[k],直到Pk=Pj时,next[j+1]=k+1
将上述思想转换为代码如下:
void getNext2(char* p, int* next) { int j, k; int m = strlen(p); next[0] = -1; next[1] = 0; k = 0; for(j=1; j<m; j++) { while(k>=0 && p[k]!=p[j]) k = next[k]; k++; next[j+1] = k; } }
至此,KMP算法的完整思想学习完毕。
KMP算法中next[]数组的其它应用:参考HDU 1358
题意:字符串S,若其某个前缀满足Ak,即前缀有k个字符串A连接而成,则输出前缀的长度和k。若某个前缀可有多个满足,则只输出最大的k
解决:假设A的长度为i,若长度为j的前缀满足Ak,即P0P1...Pi-1PiPi+1...P2i-1......P(k-1)iP(k-1)i+1...Pki-1Pj,此时j=k*i,根据上面的定义,可以知道next[j]=(k-1)*i,所以字符串A的长度i=j-next[j],k=j/i,且j%i==0
如何证明此时的循环次数k为最大?使用反证法即可,若有更大的k,再推导出已知不成立
所以本题的代码如下:
#include<stdio.h> char s[1000005]; int next[1000005]; void get_next(int n){ int i, j, k; next[0] = -1; next[1] = 0; k = 0; for(j=1; j<n; j++) { while(k >= 0 && s[j]!= s[k]) k = next[k]; k++; next[j+1] = k; } } int main() { int case_num = 0, n; int i, j, k; scanf("%d", &n); while(n) { getchar(); case_num++; scanf("%s", s); printf("Test case #%d\n", case_num); get_next(n); for(i=2; i<=n; i++) { j = i - next[i]; k = i/j; if(i%j == 0 && k > 1) { printf("%d %d\n", i, k); } } printf("\n"); scanf("%d", &n); } return 0; }
相关文章推荐
- 字符串匹配算法之KMP算法的学习
- 字符串匹配的Boyer-Moore算法 作者: 阮一峰 日期: 2013年5月 3日 上一篇文章,我介绍了KMP算法。 但是,它并不是效率最高的算法,实际采用并不多。各种文本编辑器的"查找"功能(Ct
- 字符串匹配算法——Sunday算法和KMP算法(java版本)
- 最长字符串匹配算法(KMP算法)
- KMP算法,字符串匹配算法的巅峰创意
- 字符串匹配算法之二------KMP算法
- 字符串匹配算法(二)-KMP算法
- 字符串匹配-KMP算法学习笔记
- KMP字符串匹配算法学习总结
- 字符串匹配算法--KMP算法
- swift算法实践(3)-KMP算法字符串匹配
- 字符串匹配-KMP算法学习笔记
- KMP算法和普通算法字符串匹配差距
- [算法与数据结构] - No.11 字符串匹配KMP算法
- 3. 字符串匹配算法:朴素的匹配算法、KMP算法。
- 浅谈字符串匹配算法—BF算法及KMP算法
- 算法——字符串匹配之KMP算法
- LeetCode Implement strStr()(朴素的字符串匹配,RK算法,KMP算法)
- [Algorithm] 字符串匹配算法——KMP算法
- 字符串匹配算法学习