最近看《大话数据结构》,在串这种数据结构里面提到了kmp算法,在网上搜了一下又发现了更好的Boyer-Moore算法。

这里整理一下两种算法。(此篇文章主要用于记忆)

在字符串的匹配过程中大家很容易想到从首字符一个一个的去比较,

最快的情况:test(abcdefg),pattern(abc),这里匹配了三次就找到了,时间复杂度O{1}.

最慢:test(0000001),pattern(001),每一次前面两个0都要比较一下,比较了(7-3+1)*3=15次。时间复杂度为O{(n-m+1)*m}.

显然这样的暴力匹配是很费时的。

下面来介绍一下这两种算法得思想

kmp:e于c不等,在已经匹配的字符中找到相同的前缀(ab)和后缀(ab),这样直接用前缀的后一个字符和模板中的下一个字符比较(也就是为了不使模板中的字符发生回溯)。


这里移动的距离是:已经匹配的长度(6)-相同的前后缀长度(2)=4

Boyer-Moore:这种算法直接换了一种思考模式,不再从前往后一个一个匹配,直接匹配pattern的最后一个字符,也就是从后往前匹配。不匹配则直接跳到下一个位置,匹配则往前移动到不匹配的字符,并用坏字符和好后缀算法得出移动位置。(详情见参考文章)


这里直接给出代码:

/**
      * 用于生成部分匹配表
      * 例:ABCDABD-->[0,0,0,0,1,2,0]
      * @param sub
      * @return
      */
     private static int[] kmpnext(String sub) {
         int[] n = new int[sub.length()];
         n[0]=0;
         int x = 0;
         for (int i = 1; i < sub.length(); i++) {
             while (x > 0 && sub.charAt(i) != sub.charAt(x)) {
                 x = n[x - 1];
             }


             if (sub.charAt(i) == sub.charAt(x)) {
                 x++;
             }


             n[i] = x;
         }
         return n;
     }
     
     /**
      * kmp匹配法
      * @param str
      * @param sub
      * @return
      */
     public static int kmp(String str, String sub) {
         if(str == null || sub == null || str.length() == 0 || sub.length() == 0){
             throw new IllegalArgumentException("str或者sub不能为空");
         }
         List returnList=new ArrayList();
         int j = 0;
         int[] n = kmpnext(sub);
         for (int i = 0; i < str.length(); i++) {
             while(j > 0 && str.charAt(i) != sub.charAt(j)){
             //j=j-(j-n[j-1])=n[j-1](移动位数 = 已匹配的字符数 - 对应的部分匹配值)
             /*
             * 匹配字符:abddcab,目标字符:abddcacabddcab。当b与c不等时,不需要再用a与bddca比较,直接可得出a=a,
             * 再接着比较两个a后面的字符
             * */
                 j = n[j - 1];
             }


             if(str.charAt(i) == sub.charAt(j)){
                 j++;
             }


             if(sub.length() == j){
                 int index = i - j + 2;
                 //j=0;
                 returnList.add(index);
                 return index;
             }
         }


         return -1;
     }
     /**
      * 好后缀:如果程序匹配了一个好后缀, 并且在匹配字符串中还有另外一个相同的后缀或后缀的部分, 那把下一个后缀或部分移动到当前后缀位置。
      * @author sunzhe
      * @param len,pattern,
      * */
     public static int[] moveByGood(String pattern,int len){
     int i,j;
     int[] suff = new int[len];
     int[] returnArray = new int[len];
     // 计算后缀数组
     suff = suffix(pattern,len);
     //Case3:如果完全不存在和好后缀匹配的子串,则右移整个模式串。
     for(i = 0;i < len; i++){
     returnArray[i] = len;
     }
     //Case2:如果不存在和好后缀完全匹配的子串,则在好后缀中找到具有如下特征的最长子串,使得P[m-s…m]=P[0…s]。
     
     for(i = len-1; i >= 0; i--){
     if(suff[i] == i + 1)
            {
                for(j = 0; j <= len - 1 - i - 1; j++)
                {
                    if(returnArray[j] == len)
                     returnArray[j] = len - 1 - i;
                }
            }
     } 
         //Case1:模式串中有子串和好后缀完全匹配,则将最靠右的那个子串移动到好后缀的位置继续进行匹配。
     for(i = 0; i < len - 1; i++){
     returnArray[len - 1 - suff[i]] = len - 1 - i;
     }
     return returnArray;
     }
     /**
      * 后缀数组(匹配字符部分和全部的相同个数)
      * @author
      * @param len,pattern
      * */
     public static int[] suffix(String pattern,int len){
     int[] returnArray = new int[len];
     int f,i;
     returnArray[len-1]=len;
     for(i=len-2;i>=0;i--){
     
     f=i;
     while(f>=0 && pattern.charAt(f)==pattern.charAt(len-1-i+f)) 
     f--;
     returnArray[i]=i-f;
     }
     
     return returnArray;
     }
     
     /**
      * 坏字符
      * @author sunzhe
      * @param c,pattern,j
      * */
     public static int moveByBad(int j,String pattern,char c){
     boolean flag=false;
     for(int i=0;i<pattern.length();i++){
     if(pattern.charAt(i)==c){
     flag=true;
     }
     
     }
     if(!flag){
     return j-(-1);
     }else{
     return j-last(pattern,c);
     }
     }
     /**
      * @author sunzhe
      * 搜索词中的上一次出现位置
      * */
     public static int last(String pattern,char c){
     for(int i=0;i<pattern.length();i++){
     if(pattern.charAt(i)==c && pattern.charAt(i+1)!=c){
     return i;
     }
     }
     return -1;
     }
     
     /**
      * @author sunzhe
      * Boyer-Moore算法
      * */
     public static int bM(String pattern, String test){
     int j=pattern.length()-1;
     int i=0;
     i=j;
     int[] good=moveByGood(pattern, pattern.length());
     while(i<test.length()){
     if(test.charAt(i)==pattern.charAt(j)){
     i--;
     j--;
     }else{
     char c=test.charAt(i);
     
     i=i+Math.max(moveByBad(j, pattern, c), good[j]);
     j=pattern.length()-1;
     }
     if(j==0){
     return i+1;
     }
     }
     return -1;
     }

注:


好后缀算法中先把整个数组赋值,在根据情况(if条件)修改数组中的值。这里很好体现了算法转为程序的思维。

参考文章:http://www.ruanyifeng.com/blog/2013/05/boyer-moore_string_search_algorithm.html

                    http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html

                    http://blog.jobbole.com/52830/