字符串匹配算法分析及Java实现

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

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

Brute-Force算法

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

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

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


1.  /**

2.   * Brute-Force算法

3.   *

4.   * @author stecai

5.   */

6.  public class BruteForce
{

7.

8.  /**

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

10.  *

11.  * @param source 目标字符串

12.  * @param pattern 指定字符串

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

14.  */

15. public static int 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数组对应位置的值,让Pattern中B字符对齐到String中D的字。

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位置之前,前缀和后缀没有相同的,所以值为0;4位置之前有最大前后缀A,长度为1,所以值为1,5之前有最大前后缀AB,长度为2,所以值为2。

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


1.   /**

2.    * KMPSearch 算法

3.    *

4.    * @author stecai

5.    */

6.   public class KMPSearch
{

7.       /**

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

9.        *

10.       * @param str

11.       * @return next函数值

12.       */

13.      private static int[]
calculateNext(String str) {

14.          int i
= -1;

15.          int j
= 0;

16.          int length
= str.length();

17.          int next[]
new int[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.      public static int 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
pattern下BM表现更出色。(本文用的是坏字符原则,如不理解,请看参考链接文章)

例如:模式pattern,文本string。

Pattern:  AT-THAT

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

左对齐pattern与string, 位置(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]既A在pattern中存在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.    * @author stecai

5.    */

6.   public class BoyerMoore
{

7.       /**

8.        * 计算滑动距离

9.        *

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

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

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

13.       * @return 滑动距离

14.       */

15.      private static int 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.      public static int 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.    * @author stecai

8.    */

9.   public class Sunday
{

10.      private static int currentPos =
0;

11.

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

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

14.

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

16.      public static void 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.      public static int 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.              return match(source,
pattern);

56.          } else {

57.              return currentPos;

58.          }

59.      }

60.

61.      /**

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

63.       *

64.       * @param source 目标字符串

65.       * @param pattern 指定字符串

66.       * @param pos 起始位置

67.       * @return 是否匹配

68.       */

69.      private static boolean 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.                  return false;

73.              }

74.          }

75.

76.          return true;

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

参考:

  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







时间: 2024-11-10 03:06:24

字符串匹配算法分析及Java实现的相关文章

【Java编程】Java中的字符串匹配

在Java中,字符串的匹配可以使用下面两种方法: 1.使用正则表达式判断字符串匹配 2.使用Pattern类和Matcher类判断字符串匹配 正则表达式的字符串匹配: 正则表达式:定义一组字符串的一系列字符和符号,它由常量字符和特殊符号构成. 下面是正则表达式的一些预定义字符类,实际上是一些转义字符序列: 1.\d   代表任何数字 2.\D  代表任何非数字字符 3.\w  代表任何单字字符(如:字母.数字.下划线等等) 4.\W  代表任何非单字字符 5.\s   代表任何空白字符 6.\S

关于算法--蛮力法--字符与字符串匹配

一.顺序查找 1.步骤:简单的将给定列表中的连续元素与给定的查找键作比较,直到遇到一个匹配的元素或遇到匹配元素前就遍历了整个列表 2.JavaScript代码实现 1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>SelectionFind</title> 6 </head>

CCF系列之字符串匹配(201409-3)

试题编号:201409-3试题名称:字符串匹配时间限制: 1.0s内存限制: 256.0MB 问题描述 给出一个字符串和多行文字,在这些文字中找到字符串出现的那些行.你的程序还需支持大小写敏感选项:当选项打开时,表示同一个字母的大写和小写看作不同的字符:当选项关闭时,表示同一个字母的大写和小写看作相同的字符. 输入格式 输入的第一行包含一个字符串S,由大小写英文字母组成. 第二行包含一个数字,表示大小写敏感的选项,当数字为0时表示大小写不敏感,当数字为1时表示大小写敏感. 第三行包含一个整数n,

蛮力法-顺序查找和字符串匹配

时间总让我有后知后觉的挫感,而我,总是习惯于四处张望. 3.2.1 顺序查找 将数组中的元素和给定的查找键进行比较,直到成功匹配,或者遍历完整个数组,查找失败.可将查找键添加到数组末尾,这样就不必每次循环时都检查是否到达了表的末尾(然并卵,数组不方便在添加元素吧). 代码实现: /** * 顺序查找 * @param array 对象数组 * @param key 查找键 * @return 查找成功返回元素下标,失败返回-1 * */ public static int sequentialS

Substrings(hdu1238)字符串匹配

Substrings Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)Total Submission(s): 7205 Accepted Submission(s): 3255 Problem Description You are given a number of case-sensitive strings of alphabetic characters, find the

HDU 1711 Number Sequence(字符串匹配)

Number Sequence Time Limit: 10000/5000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Submission(s): 10571    Accepted Submission(s): 4814 Problem Description Given two sequences of numbers : a[1], a[2], ...... , a[N], and b[1],

电话号码位数字符串匹配

(Message App)The app just take the last 7 digits from a contact, then it does not create a converstion with the name of the contact that you are texting with and the message is not sent M:mediatek/config/up11_ddm_a35eh/sagereal_copy/packages/apps/Mms

字符串匹配常见算法(BF,RK,KMP,BM,Sunday)

今日了解了一下字符串匹配的各种方法. 并对sundaysearch算法实现并且单元. 字符串匹配算法,是在实际工程中经常遇到的问题,也是各大公司笔试面试的常考题目.此算法通常输入为原字符串(string)和子串(pattern),要求返回子串在原字符串中首次出现的位置.比如原字符串为"ABCDEFG",子串为"DEF",则算法返回3.常见的算法包括:BF(Brute Force,暴力检索).RK(Robin-Karp,哈希检索).KMP(教科书上最常见算法).BM(

ccf题目:字符串匹配

试题编号: 201409-3 试题名称: 字符串匹配 时间限制: 1.0s 内存限制: 256.0MB 问题描述: 问题描述 给出一个字符串和多行文字,在这些文字中找到字符串出现的那些行.你的程序还需支持大小写敏感选项:当选项打开时,表示同一个字母的大写和小写看作不同的字符:当选项关闭时,表示同一个字母的大写和小写看作相同的字符. 输入格式 输入的第一行包含一个字符串S,由大小写英文字母组成. 第二行包含一个数字,表示大小写敏感的选项,当数字为0时表示大小写不敏感,当数字为1时表示大小写敏感.