字符串模式匹配算法(string searching/matchingalgorithms)
转载自http://blog.csdn.net/chndata/article/details/43792363
顾名思义,就是在一个文本或者较长的一段字符串中,找出一个或多个指定字符串(Pattern),并返回其位置。这类算法属基础算法,各种编程语言都将其包括在自带的String类函数中,而且由之衍生出来的正则表达式也是必须掌握的一种概念和编程技术。
其思路很简单:从目标字符串初始位置开始,依次分别与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算法是一种改进的字符串匹配算法,关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。在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算法是一种基于后缀匹配的模式串匹配算法,后缀匹配就是模式串从右到左开始比较,但模式串的移动还是从左到右的。字符串匹配的关键就是模式串的如何移动才是最高效的。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算法的思想和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
代码如下所示(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. } |
如下实例:
-
String: ABAC
Pattern: BAC
-
String: BBC ABCDABABCDABCDABDE
Pattern: ABCDABD
-
String: AAAAAAAAAAAAAAAAAAAAAAAAAAAAE
Pattern: AAAE
-
String: AAAAAAAAAAAAAAAAAAAAAAAAAAAAE
Pattern: CCCE
-
String: WHICH-FINALLY-HATS.--AT-THAT-POINT...
Pattern: AT-THAT
10000000次循环,时间为毫秒(ms)
| Brute-Force | KMP | Boyer-Moore | |
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里面选一种。当然,仅现于以上四种算法的选择,可能有更优的算法。
参考: