BF、kmp以及Gkmp算法
一、描述:
BF、KMP、GKMP(优化后的kmp)都是基于单个字符串(多个字符串一般要用到其他算法,比如说AC自动机)匹配的算法,不过时间复杂度各有不同,其中BF的时间复杂度为O(n * (m - n + 1)),而kmp算法的时间复杂度为O(n + m),可见快上了不少,但是这其中确实还是有不足之处,故而推出了优化后的kmp解决了回溯的重复问题。
二、BF算法分析:
BF(bruteforce)是一种暴力的模式匹配算法,其基本原理就是枚举每一种可能发生的情况,直到匹配成功,其花费的时间成本是较高的。下面我们来分析一下具体的实现过程。
首先我们有两个字符串,暂且称第一个长串为主串,称第二个短串为匹配串(子串)
T1:abcdac
T2: dac
第一轮匹配(失败)
第二轮匹配(失败)
第三轮匹配(失败)
第四轮匹配(成功,退出匹配)
代码实现:
1 #include "stdio.h" 2 #include "string.h" 3 int main() 4 { 5 char p[110];//主串 6 char q[110];//子串 7 scanf("%s%s",p,q); 8 int len1 = strlen(p); 9 int len2 = strlen(q); 10 for(int i = 0;i < len1;i++){ 11 int j = 0,m = i; 12 while(j < len2 && m < len1){ 13 if(p[m] != q[j]) 14 break; 15 m++,j++; 16 } 17 if(j == len2) 18 printf("Match Successful in %d\n",i + 1); 19 } 20 return 0; 21 }
三、KMP算法分析:
如果是长度比较短的字符串,我们可能能接受那个时间复杂度,但如果字符串的长度达到了上百万,模式串也有十万,那么匹配的时间将会是一个大麻烦,为此三位学者共同开发了这套快速的字符串匹配算法,根据他们的名字,KMP算法也由此而来。现在让我们来看看这个算法到底巧在什么地方吧!
首先给定两个字符串T1和T2
T1:abaababa
T2:abab
分析可知我们匹配这个字符串按照BF匹配的话,如果其中一个字符不匹配那么我们需要回到开头重新匹配,但是kmp算法充分利用了已知条件----T2,根据T2我们能知道某些一定不能匹配成功的位置,故可以省略,当匹配不成功的时候我们就不需要回到T2的开头重新匹配了,我们可以回到某个位置匹配,为什么呢?因为前面的位置你已经走过了呀,你已经知道哪些位置一定不可能成功匹配了,那如何能让计算机也知道呢?
首先1.我们构建一个next数组,存储不匹配时应该回到的位置(实际上就是该字符上的前缀和后缀的最大相似度+1)
2.构建一个查询函数,当j不满足匹配条件时,根据next数组回溯
虽然构建next数组时会显得浪费了时间,但是在后面查询的时候,将会带来线性阶的回馈。
接下来我们先说说什么是前缀和后缀吧
所谓前缀,就是一个字符串除了最后一个字符不参与组成子串的集合
所谓后缀,就是一个字符串除了第一个字符不参与组成子串的集合
举个例子:
abcd 前缀有a ab abc
后缀有d cd bcd
图片例子:
解释了前缀和后缀,我们来徒手建立个next数组把
T1:abaababa
T2:abab
next【5】:0 1 2 3
又因为a没有前缀也没有后缀,故先给他定为-1,专门处理回溯到子串头的匹配情况
遍历到b
前缀:a
后缀:b
前后缀相似度为0,故next[1] = 1
遍历到a
前缀:a,ab
后缀:b,ab
前后缀最大相似度为2,故next[2] = 3
遍历到b
前缀:a,ab,aba
后缀:b,ab,bab
前后缀最大相似度为2,故next[3] = 3
next数组已经构建好了,那么我么如何匹配呢?
1.碰到相等的时候i,j同时往后走
2.碰到不相等的时候j开始查询next数组回溯,直到走到那些能匹配的位置,或者是返回开头重新匹配
3.如果j已经走到子串尾后一位了,说明已经匹配成功了
代码实现:
1 #include "stdio.h" 2 #include "string.h" 3 int next[100];//next数组存的值实际上就是这个字符的前缀和后缀相似度+1 4 void Get_next(char *q) 5 { 6 int len = strlen(q);//计算模式串长度 7 next[0] = -1;//第一个字符没有前缀也没有后缀故为0 8 int i = 0,k = -1; 9 while(i < len - 1){ 10 if(k == -1 || q[i] == q[k]){//k == -1说明当前字符的前缀和后缀相似度为0,下次匹配得从头开始匹配, 11 k++; 12 i++; 13 next[i] = k; 14 } 15 else 16 k = next[k];//回溯位置 17 } 18 } 19 int kmp(char *p,char *q) 20 { 21 int len1 = strlen(p); 22 int len2 = strlen(q); 23 int i,j; 24 i = j = 0; 25 while(i < len1 && j < len2){ 26 if(j == -1 || p[i] == q[j]){ 27 i++; 28 j++; 29 } 30 else 31 j = next[j]; 32 } 33 if(j == len2) 34 return i - len2; 35 return -1; 36 } 37 int main() 38 { 39 char p[100]; 40 char q[100]; 41 scanf("%s %s",p,q);//先输入主串,再输入模式串 42 Get_next(q); 43 int L = kmp(p,q); 44 if(L != -1) 45 printf("Match successful in %d\n",L + 1); 46 else 47 printf("Match fail!"); 48 return 0; 49 }
四、Gkmp算法分析(优化后的kmp算法)
话说kmp算法已经这么好了,为什么还需要优化呢?因为kmp会执行不必要的回溯过程
比如说:
aaaaxbaaaaab
aaaaab
当第五个a失配后,会回溯到第四个
第四个很显然也会失配,会回溯到第三个a
依次类推就会回到开头了,其实这些过程都是不必要的
所以我们想如果前面的和当前字符相等时,我们的next值是否可以作优化呢?当然是可以的,当当前字符和前面的字符相等时,可以让当前字符的next值直接等于前面字符得next值,那为什么可以这样做呢?我们想啊,如果我当前字符不能满足匹配,但是我前面的能完成匹配,我是不是可以直接回到上一个和我前后缀相同的地方去?所以这就是优化的本质!
代码实现:
1 #include "stdio.h" 2 #include "string.h" 3 int nextval[110]; 4 void Get_nextval(char *q) 5 { 6 int len = strlen(q); 7 int i = 0,j = -1; 8 nextval[0] = -1;//代表匹配失败回到子串头 9 while(i < len){ 10 if(j == -1 || q[i] == q[j]){ 11 i++,j++; 12 if(q[i] != q[j]) 13 nextval[i] = j;//如果不相等,那么就不能回到j能回溯到的那个位置 14 else 15 nextval[i] = nextval[j];//如果相等,那么就能回到j能回溯到的那个位置 16 } 17 else 18 j = nextval[j];//同kmp一样的回溯 19 } 20 } 21 int Gkmp(char *p,char *q) 22 { 23 int i = 0,j = -1; 24 int len1 = strlen(p); 25 int len2 = strlen(q); 26 while(i < len1 && j < len2){ 27 if(j == -1 || p[i] == q[j]){ 28 i++; 29 j++; 30 } 31 else 32 j = nextval[j];//回溯 33 } 34 if(j == len2) 35 return i - len2 + 1; 36 return -1; 37 } 38 int main() 39 { 40 char p[100];//母串 41 char q[100];//子串 42 scanf("%s%s",p,q); 43 Get_nextval(q); 44 int L = Gkmp(p,q); 45 if(L != -1) 46 printf("Match Successful in %d\n",L); 47 else 48 printf("Match fail!\n"); 49 return 0; 50 }
五、总结
学习也必须要注重实践,“纸上谈兵终觉浅,绝知此事要躬行”。