字符串匹配算法总结 (分析及Java实现)

来源:互联网 发布:没有性格特点知乎 编辑:程序博客网 时间:2024/06/05 09:54



字符串模式匹配算法(string searching/matchingalgorithms


顾名思义,就是在一个文本或者较长的一段字符串中,找出一个或多个指定字符串(Pattern),并返回其位置。这类算法属基础算法,各种编程语言都将其包括在自带的String类函数中,而且由之衍生出来的正则表达式也是必须掌握的一种概念和编程技术。



Brute-Force算法


其思路很简单:从目标字符串初始位置开始,依次分别与Pattern的各个位置的字符比较,如相同,比较下一个位置的字符直至完全匹配;如果不同则跳到目标字符串下一位置继续如此与Pattern比较,直至找到匹配字符串并返回其位置。


我们注意到Brute Force 算法是每次移动一个单位,一个一个单位移动显然太慢,设目标串String的长度为mPattern的长度为n,不难得出BF算法的时间复杂度最坏为O(mn),效率很低。


代码也很简单,如下所示(Java)。不过,下面的代码有优化,例如21行的总的循环次数是m – n, 33行的不匹配循环终止,都让时间复杂度大为降低。


1. /**

2.  * Brute-Force算法

3.  *

4.  *@authorstecai

5.  */

6. publicclass BruteForce {

7. 

8. /**

9.  *找出指定字符串在目标字符串中的位置

10. *

11. *@param source目标字符串

12. *@param pattern指定字符串

13. *@return指定字符串在目标字符串中的位置

14. */

15.publicstaticint match(String source, String pattern) {

16.    int index = -1;

17.    boolean match =true;

18.    

19.    for (int i = 0, len = source.length() - pattern.length(); i <=len; i++) {

20.        match = true;

21.        

22.        for (int j = 0; j < pattern.length(); j++) {

23.            if (source.charAt(i + j) != pattern.charAt(j)) {

24.                match = false;

25.                break;

26.            }

27.        }

28.        

29.        if (match) {

30.            index = i;

31.            break;

32.        }

33.    }

34. 

35.    return index;

36.}

37. }


 


KMP算法


KMP算法是一种改进的字符串匹配算法,关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。在BF算法的基础上使用next函数来找出下一次目标函数与Pattern比较的位置,因为BF算法每次移动一位的比较是冗余的,KMP利用Pattern字符重复的特性来排除不必要的比较,从而可以每次移动n位来排除冗余。对于Next函数近似接近O(m)KMP算法的时间复杂度为O(n),所以整个算法的时间复杂度为O(n+m)


例如:模式pattern,文本string


Pattern: ABCAC

String:  ABCADCACBAB


在红色字体处发生失配,按照传统算法,应当从第二个字符 B 对齐再进行匹配,这个过程中,对字符串String的访问发生了回朔我们不希望发生这样的回朔,而是试图通过尽可能的向右滑动模式串next数组对应位置的值,让PatternB字符对齐到StringD的字。


Pattern:       ABCAC

String:  ABCADCACBAB


因此,问题的关键是计算向右引动的串的模式值next[]。模式串开始为值(既next[0])为-1,后面的任一位置例如j,计算j之前(既0 ~ j-1)中最大的相同的前后缀的字符数量,即为next数组j位置的值。例如:


位置 j

0

1

2

3

4

5

模式串

A

B

C

A

B

D

next[]

-1

0

0

0

1

2


从上表可以看出, 3位置之前,前缀和后缀没有相同的,所以值为04位置之前有最大前后缀A,长度为1,所以值为15之前有最大前后缀AB,长度为2,所以值为2


KMP虽然经典,很不容易理解,即使理解好了,编码也相当麻烦!特别是计算next数组的部分。代码如下所示,核心是next[]数组的得出方法:


1.  /**

2.   * KMPSearch算法

3.   *

4.   *@authorstecai

5.   */

6.  publicclass KMPSearch {

7.     /**

8.      * 获得字符串的next函数值

9.      *

10.     * @param str

11.     * @return next函数值

12.     */

13.    privatestaticint[] calculateNext(String str) {

14.        int i = -1;

15.        int j = 0;

16.        int length = str.length();

17.        int next[] =newint[length];

18.        next[0] = -1;

19.        

20.        while (j < length - 1) {

21.            if (i == -1 || str.charAt(i) == str.charAt(j)) {

22.                i++;

23.                j++;

24.                next[j] = i;

25.            } else {

26.                i = next[i];

27.            }

28.        }

29.        

30.        return next;

31.    }

32.  

33.    /**

34.     * KMP匹配字符串

35.     *

36.     * @param source目标字符串

37.     * @param pattern指定字符串

38.     * @return若匹配成功,返回下标,否则返回-1

39.     */

40.    publicstaticint match(String source, String pattern) {

41.        int i = 0;

42.        int j = 0;

43.        int input_len = source.length();

44.        int kw_len = pattern.length();

45.        int[] next =calculateNext(pattern);

46.        

47.        while ((i < input_len) && (j < kw_len)) {

48.            //如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++j++

49.            if (j == -1 || source.charAt(i) == pattern.charAt(j)) {

50.                j++;

51.                i++;

52.            } else {

53.                //如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i不变,j = next[j],

54.                // next[j]即为j所对应的next

55.                j = next[j];

56.            }

57.        }

58.        

59.        if (j == kw_len) {

60.            return i - kw_len;

61.        } else {

62.            return -1;

63.        }

64.    }

65.    }

 


Boyer-Moore算法


Boyer-Moore算法是一种基于后缀匹配的模式串匹配算法,后缀匹配就是模式串从右到左开始比较,但模式串的移动还是从左到右的。字符串匹配的关键就是模式串的如何移动才是最高效的。BM的时间复杂度,最好O(n/m),最坏O(n),通常在longer patternBM表现更出色。(本文用的是坏字符原则,如不理解,请看参考链接文章)


例如:模式pattern,文本string


Pattern: AT-THAT

String:  WHICH-FINALLY-HATS.--AT-THAT-POINT...


左对齐patternstring位置(p)指向对齐后的右end,开始比对。如果pattern [p]= string[p],那么往左移动(移到左start说明匹配上了),否则就要移动pattern进行重新对齐,重新对齐后,进行重新比对。有两种情况:


  • 末位不匹配,且string[p]pattern中不存在,那么pattern可以一下子右移patlen个单位。

    Pattern:                AT-THAT

    String:   WHICH-FINALLY-HATS.--AT-THAT-POINT...

  • 末位不匹配,但string[p]pattern中存在,例如上边T-(如果有多个,那就找最靠右的那个),距pattern右端为(patlen – 最右边那个Pattern[p])的位置。

    Pattern:                 AT-THAT

    String:     WHICH-FINALLY-HATS.--AT-THAT-POINT...

  • 部分匹配,下例绿色部分AT相同,但string[p]Apattern中存在2个位置,很显然如果我们用最右边的那个A既已经被匹配正确的,那么就会产生回退。因此我们应该用左边的那个,既匹配不成功位置之前最右边的那个。距pattern右端为(patlen –既匹配不成功位置之前最右边的那个Pattern [p])的位置。


  • 移动前

    Pattern:         AT-THAT

    String:     WHICH-FAATNALLY-HATS.--AT-THAT-POINT...

  • 移动后

    Pattern:                  AT-THAT

    String:     WHICH-FAATNALLY-HATS.--AT-THAT-POINT...

     

此为简化版的算法,事实上部分匹配还有更优化的最大右移量。在此就不做深入研究了。


代码如下所示(Java)


1.  /**

2.   * Boyer-Moore算法

3.   *

4.   *@authorstecai

5.   */

6.  publicclass BoyerMoore {

7.     /**

8.      * 计算滑动距离

9.      *

10.     * @param c主串(源串)中的字符

11.     * @param T模式串(目标串)字符数组

12.     * @param noMatchPos上次不匹配的位置

13.     * @return滑动距离

14.     */

15.    privatestaticint dist(char c, char T[],int noMatchPos) {

16.        int n = T.length;

17.        

18.        for (int i = noMatchPos; i >= 1; i--) {

19.            if (T[i - 1] == c) {

20.                return n - i;

21.            }

22.        }

23.        

24.        // c不出现在模式中时

25.        return n;

26.    }

27.  

28.    /**

29.     * 找出指定字符串在目标字符串中的位置

30.     *

31.     * @param source目标字符串

32.     * @param pattern指定字符串

33.     * @return指定字符串在目标字符串中的位置

34.     */

35.    publicstaticint match(String source, String pattern) {

36.        char[] s = source.toCharArray();

37.        char[] t = pattern.toCharArray();

38.        int slen = s.length;

39.        int tlen = t.length;

40.        

41.        if (slen < tlen) {

42.            return -1;

43.        }

44.  

45.        int i = tlen;

46.        int j = -1;

47.        

48.        while (i <= slen) {

49.            j = tlen;

50.            // S[i-1]T[j-1]若匹配,则进行下一组比较;反之离开循环。

51.            while (j > 0 && s[i - 1] == t[j - 1]) {

52.                i--;

53.                j--;

54.            }

55.            

56.            // j=0时,表示完美匹配,返回其开始匹配的位置

57.            if (0 == j) {

58.                return i;

59.            } else {

60.                //把主串和模式串均向右滑动一段距离dist(s[i-1]).

61.                i = i + dist(s[i - 1], t, j - 1);

62.            }

63.        }

64.        

65.        //模式串与主串无法匹配

66.        return -1;

67.    }

68.  }


Sunday算法


Sunday算法的思想和BM算法中的坏字符思想非常类似。差别只是在于Sunday算法在匹配失败之后,是取String串中当前和Pattern字符串对应的部分后面一个位置的字符来做坏字符匹配。当发现匹配失败的时候就判断母串中当前偏移量+Pattern字符串长度(假设为K位置)的字符在Pattern字符串中是否存在。如果存在,则将该位置和Pattern字符串中的该字符对齐,再从头开始匹配;如果不存在,就将Pattern字符串向后移动,和母串k处的字符对齐,再进行匹配。重复上面的操作直到找到,或母串被找完结束。


该算法最坏情况下的时间复杂度O(NM)。对于短模式串的匹配问题,该算法执行速度较快。


例如:模式pattern,文本string


Pattern:ATTHAT

String:    AHICHTANALLY-HATS.--AT-THAT-POINT...


  • 我们看到A-H没有对上,我们就看匹配串中的A在模式串的位置

    Pattern:   ATTHAT

    String:  AHICHTANALLY-HATS.--AT-THAT-POINT...

  • 如果模式串中的没有那个字符,跳过去。

    Pattern:           ATTHAT

    String:AHICHTENALLY-HATS.--AT-THAT-POINT...


代码如下所示(Java)


1.  import java.util.HashMap;

2.  import java.util.Map;

3.   

4.  /**

5.   * Sunday算法

6.   *

7.   *@authorstecai

8.   */

9.  publicclass Sunday {

10.    privatestaticintcurrentPos = 0;

11.  

12.     //匹配字符的Map,记录改匹配字符串有哪些char并且每个char最后出现的位移

13.     privatestatic Map<Character, Integer> map =new HashMap<Character, Integer>();

14.  

15.     // Sunday匹配时,用来存储Pattern中每个字符最后一次出现的位置,从右到左的顺序

16.     publicstaticvoid initMap(String pattern) {

17.     for (int i = 0, plen = pattern.length(); i < plen; i++) {

18.             map.put(pattern.charAt(i), i);

19.         }

20.     }

21.  

22.     /**

23.      * Sunday匹配,假定Text中的K字符的位置为:当前偏移量+Pattern字符串长度+1

24.      *

25.      *@param source目标字符串

26.     * @param pattern 指定字符串

27.     * @return指定字符串在目标字符串中的位置

28.      */

29.    publicstaticint match(String source, String pattern) {

30.        int slen = source.length();

31.        int plen = pattern.length();

32.        

33.        //当剩下的原串小于指定字符串时,匹配不成功

34.        if ((slen -currentPos) < plen) {

35.            return -1;

36.        }

37.        

38.        //如果没有匹配成功

39.        if (!isMatchFromPos(source, pattern, currentPos)) {

40.            int nextStartPos = currentPos + plen;

41.            

42.            //如果移动位置正好是结尾,即是没有匹配到

43.            if ((nextStartPos) == slen) {

44.                return -1;

45.            }

46.            

47.            //如果匹配的后一个字符没有在Pattern字符串中出现,则跳过整个Pattern字符串长度

48.            if (!map.containsKey(source.charAt(nextStartPos))) {

49.                currentPos = nextStartPos;

50.            } else {

51.                //如果匹配的后一个字符在Pattern字符串中出现,则将该位置和Pattern字符串中的最右边相同字符的位置对齐

52.                currentPos = nextStartPos - (Integer) map.get(source.charAt(nextStartPos));

53.            }

54.  

55.            returnmatch(source, pattern);

56.        } else {

57.            returncurrentPos;

58.        }

59.    }

60.  

61.    /**

62.     * 检查从Text的指定偏移量开始的子串是否和Pattern匹配

63.     *

64.     * @param source 目标字符串

65.     * @param pattern 指定字符串

66.     * @param pos 起始位置

67.     * @return是否匹配

68.     */

69.    privatestaticboolean isMatchFromPos(String source, String pattern, int pos) {

70.        for (int i = 0, plen = pattern.length(); i < plen; i++) {

71.            if (source.charAt(pos + i) != pattern.charAt(i)) {

72.                returnfalse;

73.            }

74.        }

75.  

76.        returntrue;

77.    }

78.  }


 


运行实例比较


如下实例:


  1. String:  ABAC

    Pattern:BAC

  2. String: BBC ABCDABABCDABCDABDE

    Pattern:ABCDABD

  3. String: AAAAAAAAAAAAAAAAAAAAAAAAAAAAE

    Pattern:AAAE

  4. String: AAAAAAAAAAAAAAAAAAAAAAAAAAAAE

    Pattern:CCCE

  5. String: WHICH-FINALLY-HATS.--AT-THAT-POINT...

    Pattern:AT-THAT


10000000次循环,时间为毫秒(ms


 

Brute-Force

KMP

Boyer-Moore

Sunday

1

202

685

828

651

2

1468

2197

1284

1231

3

3493

3978

2752

777

4

1425

3669

1481

629

5

1742

3503

1504

1253



从上面的结果来看:


  • KMP, Boyer-Moore, Sunday相比较,很很明显的性能差异。既KMP < Boyer-Moore < Sunday.

  • KMP, Boyer-Moore, Sunday都有对pattern串的预处理,像KMPnextBoyer-Mooredist,以及Sundaymap生成,要耗费部分资源,在某些情况下,例如上面的1的情况(sourcepattern长度差不是很大,及上面提到的没有达到最大的时间复杂度),Brute-Force能达到很好效果。是否能有好的性能,主要是看它移动的幅度消耗的性能是否能抵消对pattern串的预处理,个人建议,在Brute-ForceSunday里面选一种。当然,仅现于以上四种算法的选择,可能有更优的算法。


 


参考:


  1. http://en.wikipedia.org/wiki/String_searching_algorithm

  2. http://blog.sina.com.cn/s/blog_4b241f500102v4l6.html

  3. http://blog.csdn.net/iJuliet/article/details/4200771







0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 户户通001信号中断怎么办 电脑运行速度特别慢怎么办 win8.1电脑太卡怎么办 电线厂非法战地没拆够怎么办 久笔记本电脑玩彩虹六号卡怎么办 彩虹六号一直建立小队进不去怎么办 小佩喂食器离线怎么办 手机打游戏掉帧怎么办 电脑打游戏掉帧怎么办 武装突袭3太卡怎么办 英语b级考不过怎么办 绝地求生被燃烧瓶烧了怎么办 搜狗输入法打字出现问好怎么办 全民k歌解码失败怎么办 视频声音小怎么办调大 乐视2视频声音小怎么办 录视频声音太小怎么办 显卡装了没反应怎么办 笔记本关闭核显黑屏怎么办 驱动补丁被卸了怎么办 网络驱动被删了怎么办 新装系统网卡没驱动怎么办 核显没有dp口怎么办 苹果7屏幕太小了怎么办 苹果装系统卡住了怎么办 苹果7手机白屏黑苹果怎么办 衣服破了个洞怎么办 黑苹果自带升级怎么办 双显卡都禁用了怎么办 win10优盘识别不出来怎么办 黑苹果无限重启怎么办 水泥地面起砂怎么办?这几招很有用 跟老公没法过了怎么办 胃和肠子有点烂怎么办? 车的屏幕黑了怎么办 ios12玩游戏闪退怎么办 没网steam要登录怎么办 电脑有gta还需要安装怎么办 孤岛惊魂5卡顿怎么办 酷匠密码忘记了怎么办 战地1有时候卡死怎么办