子串匹配问题。
朴素算法:将子串中每一个字符与主串进行对比,子串对比完说明在主串中有此子串,输出当前比较的位数减去子串的长度,即子串在主串中出现的位数。若主串比较完、说明主串中没有查找的子串。输出零。
既然是朴素算法,效率低是必然的。KMP算法解决了朴素算法中重复计算的弊端。利用培训教程里的例子:
主串:G A C G A T A T A A G C C A G
子串:A T AT A C G
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
G |
A |
C |
G |
A |
T |
A |
T |
A |
A |
G |
C |
C |
A |
G |
|
A |
T |
A |
T |
A |
C |
G |
|
|
A |
T |
A |
T |
A |
C |
G |
【上面一行博客里格式对不齐,将最后一行的首字母对齐到7那里在看】
第一次匹配中比较到第十位不匹配后,朴素算法应是继续从主串第六位开始,与子串第一位开始比较。但分析子串可以看出,t[7]t[8]=t[9]t[10],此时可以将子串“滑动到”主串的第七位,将主串第九位与子串第三位进行比较,以此来减少比较的次数。
由此,我们需要一个数组来记录子串中相同的“子子串”的位置。例如例子中的“AT”。将此数组设为NEXT,NEXT数组的值如下表。
1 |
2 |
3 |
4 |
5 |
6 |
7 |
A |
T |
A |
T |
A |
C |
G |
0 |
1 |
1 |
2 |
3 |
4 |
1 |
注释:NEXT[1]=0.//必须。
NEXT[2]=1,当子串与主串比较到子串的第二位不匹配时,转到子串的第1位进行比较。因为第二位之前没有与开头字母相匹配的子子串。
NEXT[3]=1,同上。
NEXT[4]=2,当匹配到第四位时若不匹配可从第三位起与主串的下一位继续比较。因为第四位前存在A=A。即t[1]=t[3]。下一次比较前已经知道t[1]与主串的下一位相同,省去了再次比较的麻烦。
NEXT[5]=3,t[1]t[2]=t[3]t[4],下次可从第三位开始比较。
依此类推。
关于NEXT数组的创建,则相当于主串与子串的不断比较过程。执行过程的原理是相同的。
program kmp; var i,j,n:longint; s,t:ansistring; next:array[1..1000001]of longint; procedure getnext(t:ansistring);//Next数组的创建 var j,k:integer; begin j:=1; k:=0;//初始化,j为比较的子串的下标,k为寻找的相同的子子串的下标 while j<length(t) do //如果子串未比较完,进入循环查找子子串 if (k=0)or(t[j]=t[k]) then //k=0即第一位,或者子子串在子串中找到匹配,则继续往后比较 begin inc(j); inc(k);//两个下标都向后移一位。 next[j]:=k;//将匹配完后的值赋值给Next数组。 end else k:=next[k];//如果不匹配,字字串的下标转到需要比较的那一位,避免重复查找。 end; function index(s:ansistring;t:ansistring):longint; //计算子串出现的位数 var i,j:longint; begin getnext(t);//Next数组取值。 index:=0;//若没有子串的匹配则输出0. i:=1; j:=1;//i是主串的下标,j为子串的下标。 while (i<=length(s))and(j<=length(t)) do//主串与子串都未比较完就继续查找。 begin if (j=0)or(s[i]=t[j]) then //与getnext的原理相同。 begin inc(i); inc(j); end else j:=next[j]; if j>length(t) then index:=i-length(t);//如果子串比较完,则子串在主串中有匹配,此时输出的应是当前位减去子串的长度,即子串在主串出现的位数。 end; end; begin readln(s); //输入主串 readln(t); //输入子串 writeln(index(s,t)); //index函数计算子串在主串中出现的位数并输出。 end.
最坏的时间复杂度:O(M+N),M和N分别是主串和子串的长度。