讨论了一下午绕晕了都没搞懂了算法,看了视频12分钟基本搞懂了
附上B站视频链接:https://www.bilibili.com/video/av3246487?from=search&seid=5216993177757720410
先从暴力搜索开始说起,暴力搜索的匹配模式如下:
初始状态:
- 如果当前字符匹配成功(即S[i] == P[j]),则i++,j++,继续匹配下一个字符;
- 如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0。
- 而S[1]肯定跟P[0]失配。为什么呢?因为在之前第1步匹配中,我们已经得知S[1] = P[1] = b,而P[0] = a,即P[1] != P[0],故S[1]必定不等于P[0],所以回溯过去必然会导致失配。暴力匹配的时间复杂度为O(m*n),就像刚才所说,暴力匹配在回溯的过程中执行了很多不必要的步骤,导致时间复杂度加大,那用什么算法可以减少暴力匹配中不必要的回溯呢?
KMP算法:
KMP算法的与暴力匹配的最大区别点就是:当到达不匹配的位置时i是不变的,只有模式串自身在回溯,那具体模式串的j要回溯到什么位置,现在就要引入一个数组next[],当s[i]!=p[j]时,令j=next[j]。而KMP算法最核心的部分就是next数组的求法,有很多种不同的找法,其实思想也差不多,只是每个人的理解不同。
接下来着重介绍next[]数组是如何得到的:
观察发现,j回溯的位置与模式串自身的结构有很大的关系,此处介绍前缀和后缀的概念:
- 如果给定的模式串是:“abcaby”,从左至右遍历整个模式串,其各个子串的前缀后缀分别如下表格所示:
字符串 | 前缀 | 后缀 | 最大公共部分 | 最大公共部分长度 |
a | 空 | 空 | 无 | 0 |
ab | a | b | 无 | 0 |
abc | a,ab | c,bc | 无 | 0 |
abca | a,ab,abc | a,ca,bca | a | 1 |
abcab | a,ab,abc,abca | b,ab,cab,bcab | ab | 2 |
abcaby | a,ab,abc,abca,abcab | y,by,aby,caby,bcaby | 无 | 0 |
- 得到最大长度表:(用数组n[]表示,与next[]数组密切相关)
下标
0
1
2
3
4
5
下标对应字符
a
b
c
a
b
c
n[i]
0
0
0
1
2
0
我们结合最大长度表的意义可以发现,当匹配过程中出现s[i]!=p[j]时i,本应该执行j=0,但公共长度=前缀和后缀相等部分的长度,既然后缀已经匹配了,那前缀也一定匹配,因此直接跳过公共部分,所以应该从字符串头开始跳过j之前字符串的最大公共长度(即p[j-1]) 个字符,即:j=n[j-1];
所以,观察可以发现,next[]数组起始其实就是n[]数组依次往右移一位所构成的,由于next[0]没有元素,因此我们把next[0]初始化为-1,next数组就已经完成构建了。
n[]数组构建步骤:
下面附上得到next[]数组的代码:
1 int GetNext(string p, int plen)//匹配串自己和自己匹配的过程 2 { 3 int n[1000]; 4 int i = 0, j = 1;//i在首位上 5 n[0] = 0;//n[0]初始化为0 6 next[c++]=-1; 7 while (j < plen)//循环条件为模式串p遍历结束 8 { 9 //从头开始比较,寻找到公共部分 10 if (p[i] == p[j]) {//若发现当前字符也是公共前缀 11 n[j] = i + 1;//在之前已找到公共前缀的下标加1(记录当前已知公共部分的长度) 12 i++; j++;//若相同,i,j同时移动 13 } 14 else { 15 if (i == 0)n[j] = 0; 16 else i = n[i - 1];//若遇到与前缀的最后一个定位符不匹配的字符,应该回溯,去找上一个最大前缀的最大公共部分 17 //不断前回溯,直到匹配或者i回到第一个字符 18 } 19 next[c++]=n[i]; 20 } 21 return 0; 22 }
KMP匹配过程:
KMP算法中循环结束的标志是j<plen
1 int KMPsearch(string t, string p) 2 { 3 int plen = p.length(), tlen = t.length(); 4 GetNext(p, plen); 5 int i=0, j=0; 6 while (i < tlen&&j < plen) 7 { 8 if (t[i] == p[j])//匹配,同时移动i,j 9 { 10 i++; j++; 11 } 12 else //不匹配,移动j,i不动 13 { 14 if (j == 0)i++; 15 else j = next[j]; 16 } 17 } 18 if (j >= plen)return i - j; 19 else return -1; 20 }
完整代码:
1 #include<iostream> 2 #include<cstring> 3 using namespace std; 4 int next[1000],c=0; 5 int GetNext(string p, int plen)//匹配串自己和自己匹配的过程 6 { 7 int n[1000]; 8 int i = 0, j = 1;//i在首位上 9 n[0] = 0;//n[0]初始化为0 10 next[c++]=-1; 11 while (j < plen)//循环条件为模式串p遍历结束 12 { 13 //从头开始比较,寻找到公共部分 14 if (p[i] == p[j]) {//若发现当前字符也是公共前缀 15 n[j] = i + 1;//在之前已找到公共前缀的下标加1(记录当前已知公共部分的长度) 16 i++; j++;//若相同,i,j同时移动 17 } 18 else { 19 if (i == 0)n[j] = 0; 20 else i = n[i - 1];//若遇到与前缀的最后一个定位符不匹配的字符,应该回溯,去找上一个最大前缀的最大公共部分 21 //不断前回溯,直到匹配或者i回到第一个字符 22 } 23 next[c++]=n[i]; 24 } 25 return 0; 26 } 27 28 int KMPsearch(string t, string p) 29 { 30 int plen = p.length(), tlen = t.length(); 31 GetNext(p, plen); 32 int i=0, j=0; 33 while (i < tlen&&j < plen) 34 { 35 if (t[i] == p[j])//匹配,同时移动i,j 36 { 37 i++; j++; 38 } 39 else //不匹配,移动j,i不动 40 { 41 if (j == 0)i++; 42 else j = next[j]; 43 } 44 } 45 if (j >= plen)return i - j; 46 else return -1; 47 } 48 int main() 49 { 50 string t, p; 51 t = "abcdefgggghhhjjj"; 52 p = "jjj"; 53 cout << KMPsearch(t, p); 54 return 0; 55 }
输出结果: