字符串匹配算法总结 (分析及Java实现)
来源:互联网 发布:没有性格特点知乎 编辑:程序博客网 时间:2024/06/05 09:54
字符串模式匹配算法(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. *@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数组对应位置的值,让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. *@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 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. *@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. }
运行实例比较
如下实例:
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
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里面选一种。当然,仅现于以上四种算法的选择,可能有更优的算法。
参考:
http://en.wikipedia.org/wiki/String_searching_algorithm
http://blog.sina.com.cn/s/blog_4b241f500102v4l6.html
http://blog.csdn.net/iJuliet/article/details/4200771
- 字符串匹配算法总结 (分析及Java实现)
- KMP算法(字符串匹配算法)详解及java实现
- 字符串匹配算法的学习及分析
- KMP字符串匹配算法的分析实现
- 算法 字符串匹配之朴素算法和KMP算法及JAVA代码实现
- 字符串匹配算法总结
- 字符串匹配算法总结
- 字符串匹配算法总结
- 字符串匹配算法总结
- 字符串匹配算法总结
- 字符串匹配算法总结
- 字符串匹配算法总结
- 字符串匹配算法总结
- 字符串匹配算法总结
- 字符串匹配算法总结
- 字符串匹配算法总结
- 字符串匹配算法总结
- 字符串匹配算法总结
- poj 3259 Wormholes[ bellman_ford 判负环]
- Valgrind
- 基于JQuery easyUI combobox实现了一个类似google、百度输入框的提示
- 我的商铺项目总结
- 每天一点数据库之-----Day 6 数据分组与数据分页
- 字符串匹配算法总结 (分析及Java实现)
- 高斯模糊算法
- Delphi 导出、导入Excel的一个快速方法
- [LeetCode]Delete Digits
- 算法精解十一(C语言版)
- SpringMvc定时器任务
- apue学习第二十四天——高级I/O、生产者消费者问题、socket(提纲)
- Android加载大图、多图OOM解决方案
- zTree -- jQuery 树插件(后台异步获取数据-asp.net mvc模式下)