基本的模式匹配算法
假设现在有主串S=s1,...,sn,,模式串P=p1,...,pm,基本的模式匹配算法是将P中的字符p1与S中的字符s1比较,如果相等,则依次递增比较pi+1和sj+1。如果不相等,则将p1与S中的字符s2比较,依次类推。如果存i=m,则存在匹配模式,否则匹配失败。
KMP算法
KMP算法是由由D.E.Knuth与V.R.Pratt和J.H.Morris同时发现,所以人们称它为克努特—莫里斯—普拉特算法,该算法的时间复杂度为O(n+m),n为主串的长度,m为模式串的长度。
KMP算法的特点是不需要回潮对主串的匹配,核心思想是如果模式串当前字符a与主串当前的字符b不匹配,分两种情况,如果对于模式串中字符c之前的一个子串s,存在一个以模式串首位开始与子串s相同的子串s',则对于之前子串s其实已经通过其之后子串s'完成匹配了,所以不再需要比较,则从模式串的中间位置开始比较。如果不存在这样的子串s',则说明字符b之前开始的子串都不与模式串匹配。
算法思路:该算法的关键是当不匹配时,下一次比较将模式串的第几位开始。根据前面的分析,该位置与模式串相关,由于和主串的比较还是依次从头至尾 的,模式串重新比较的位置与主串无关,这就意味着模式串不变时,模式串中下一个匹配的位置是确定的,而不受主串影响。
对于模式串P=p1,...,pi-1,pi,...,pm,如果模式串的子串p1,...,pi-1,已经和主串S=s1,...,sn中子串sj,...,sj+i-1匹配成功,而pi与sj+i不相等,即不匹配,则我们要使得j=j+1,即将模式串与主串中当前位置的下一个位置的字符串起开始比较。正如前文所说的,我们不是重新从模式串的首位开始比较,而跳跃地从一个中间位置开始比较。我们用一个长度为m的数组next[m]保存模式串中当每一个位置的字符匹配失败后,下一次模式串重新开始比较的位置。
next的值事实上是针对当模式串P中第j位的字符P[j]与主串匹配失败时计算的,分为三种情况:
第一种,next[i] = -1,当i=0时;
第二种next[i] = max{k|1<k<i且p[0],...,p[k-1]=[i-k],...p[i-1]};
其他情况,next[i] = 0
这该算法的JAVA源码如下:
-
-
-
-
-
-
- public class KMP1 {
-
-
- public int match(String p,String t){
- String pattern = p;
- String text = t;
- int i = 0, j = 0;
- int[] next = calculate_next(pattern);
- while(i < pattern.length()&& j <text.length()){
- if(i==-1|| pattern.charAt(i)==text.charAt(j)){
- ++i;
- ++j;
- }
- else{
- i = next[i];
- }
- }
- if(j > pattern.length())
- return j-pattern.length();
- else return 0;
- }
-
- public int[] calculate_next(String pattern){
- int m = pattern.length();
- System.out.println(m);
- int[] next = new int[m];
- int i = 0,j = -1;
- next[0] = -1;
- while(i<m-1){
- if(j==-1||pattern.charAt(i)==pattern.charAt(j)){
- ++i;
- ++j;
- next[i] = j;
- }
- else{
- j = next[j];
- }
- }
- return next;
- }
-
-
-
-
- public static void main(String[] args) {
-
- String pattern = "abcfdabceg";
- String text = "egaradfaababcfdabcegeabcbcae";
- KMP1 run = new KMP1();
- int position = run.match(pattern,text);
- System.out.println(position);
-
- }
- }
细心的读者也许会发现calculate_next函数和match函数十分类似,其实对于next的计算其实已经将模式串即当作主串又当作模式串来计算的。因为KMP算法本质其实是考虑到模式串中有相同子串,前面的子串和后面的子串相同,如果后面的子串已经是匹配成功的时,前面的子串必定和相应的主串是相匹配的。
仔细分析以下发现其实next值的计算还有改时,可以减少对主串的比较,改进后的next值的计算,分为三种情况:
第一种,如果这p[i]=p[0],则next[i]=-1。表明模式串的当前位置与模式串的首位相同,当模式串当前位置与主串的当前位匹配失败时,则模式串的首位与主串的当前位也会匹配失败,则模式串的首位应和主串当前位的下一位比较,即主串当前位向右移一位。模式串当前位为首位。
第二种,如果第一种情况不满足时,当p[0],...,p[k-]=p[i-k],...p[i],k>=0,则next[i] = 0。表明如果模式串当前位置为结束位的一个子串与模式串的首位开始的一个子串相等,如果模式串当前位置匹配失败,则表明模式串首位开始的那个子串也会匹配失败,则应从模式串的首位重新开始匹配。
第三种,如果前两情况不满足时,当p[0],...,p[k-1]=[i-k],...p[i-1]且p[k]!=p[i],k>=1,则next[i] =k-1。表明如果模式串当前位置之前的一个子串与模式串的首位开始的一个子串相等,如果模式串当前位置匹配失败,但是模式串匹配失败位置之前子串是匹配成功的,且与模式串首位开始的子串是匹配的,所以应相应从模式串的首位开始的子串的结束位开始匹配。
以下改进后的KMP算法的Java源码:
-
-
-
-
-
-
- public class KMP2 {
-
-
- public int match(String p,String t){
- String pattern = p;
- String text = t;
- int i = 0, j = 0;
- int[] next = calculate_next(pattern);
- while(i < pattern.length()&& j <text.length()){
- if(i==-1|| pattern.charAt(i)==text.charAt(j)){
- ++i;
- ++j;
- }
- else{
- i = next[i];
- }
- }
- if(j > pattern.length())
- return j-pattern.length();
- else return 0;
- }
-
- public int[] calculate_next(String pattern){
- int m = pattern.length();
- int[] next = new int[m];
- next[0] = -1;
-
- System.out.println("i="+0+" "+"-1");
- int i = 1,j = 0;
- while(i<m){
- if(pattern.charAt(i)==pattern.charAt(0)){
- next[i] = -1;
- System.out.println("i="+i+" "+next[i]);
- i++;
- j++;
- }
- else if(pattern.charAt(i)!=pattern.charAt(j)){
- next[i] = j;
- System.out.println("i="+i+" "+next[i]);
- i++;
- j = 0;
- }
- else {
- if(pattern.charAt(i)==pattern.charAt(j)){
- i++;
- j++;
- }
- next[i] = 0;
- System.out.println("i="+i+" "+next[i]);
- }
- }
- return next;
- }
-
-
-
-
- public static void main(String[] args) {
-
- String pattern = "abcfdabceg";
- String text = "egaababcfdabcegeabcbcae";
- KMP2 run = new KMP2();
- int position = run.match(pattern,text);
- System.out.println(position);
- }
- }