经典模式匹配(转)

来源:互联网 发布:group_concat mysql 编辑:程序博客网 时间:2024/06/12 09:25

从一个很长的字符串(或者数组)中,查找某个子串(模式串)是否存在,在算法上被称为是模式匹配 

模式匹配的经典算法包括KMP算法BM算法等等。以下简要回顾这些经典算法的思想,并说明我对此的改进想法。 

KMP算法


首先对模式串进行处理,获得当某个字符位置失配的时候,比较位置的指针应该指向的下一个位置的数组。举例如下: 

Pseud代码 

1.      模式串:A B A B C  

2.      next 0 0 1 2 0  



经过对上面的模式串进行处理,我们可以做如下比较: 

Pseud代码 

1.      索引位:_ _ ▼ ↓  

2.      字符串:A B A A B A B D C A B A B A B C  

3.      模式串:A B A B C  

4.      比较位:_ _ ▲ ↑ 失配,指针指向 next[3] = 1 的位置  

5.        

6.      索引位:_ _ ▼ ↓  

7.      字符串:A B A A B A B D C A B A B A B C  

8.      模式串:_ _ A B A B C  

9.      比较位:    ▲ ↑ 仍然失配,next[1] = 0 的位置  

10.    

11.  索引位:_ _ ▼ ↓  

12.  字符串:A B A A B A B D C A B A B A B C  

13.  模式串:_ _ _ A B A B C  

14.  比较位:    ▲ ↑ OK了,继续向下走。  

15.    

16.  索引位:_ _ _ _ _ _ ▼ ↓  

17.  字符串:A B A A B A B D C A B A B A B C  

18.  模式串:_ _ _ A B A B C  

19.  比较位:      _ _ _ ▲ ↑ 失配,next[4] = 2 的位置。  

20.    

21.  索引位:_ _ _ _ _ _ ▼ ↓  

22.  字符串:A B A A B A B D C A B A B A B C  

23.  模式串:_ _ _ _ _ A B A B C  

24.  比较位:          _ ▲ ↑ 仍然失配,next[2] = 0 的位置。  

25.    

26.  索引位:_ _ _ _ _ _ ▼ ↓  

27.  字符串:A B A A B A B D C A B A B A B C  

28.  模式串:_ _ _ _ _ _ _ A B A B C  

29.  比较位:            ▲ ↑ 仍然失配且比较位已经是0了,只能索引,比较+1继续找。  

30.    

31.  索引位:_ _ _ _ _ _ _ ▼ ↓  

32.  字符串:A B A A B A B D C A B A B A B C  

33.  模式串:_ _ _ _ _ _ _ _ A B A B C  

34.  比较位:              ▲ ↑ 仍然失配且比较位已经是0了,只能索引,比较+1继续找。  

35.    

36.  索引位:_ _ _ _ _ _ _ _ ▼ ↓  

37.  字符串:A B A A B A B D C A B A B A B C  

38.  模式串:_ _ _ _ _ _ _ _ _ A B A B C  

39.  比较位:                ▲ ↑ OK,继续向下走。  

40.    

41.  索引位:_ _ _ _ _ _ _ _ _ _ _ _ ▼ ↓  

42.  字符串:A B A A B A B D C A B A B A B C  

43.  模式串:_ _ _ _ _ _ _ _ _ A B A B C  

44.  比较位:                  _ _ _ ▲ ↑ 失配,next[4] = 2 的位置。  

45.    

46.  索引位:_ _ _ _ _ _ _ _ _ _ _ _ ▼ ↓  

47.  字符串:A B A A B A B D C A B A B A B C  

48.  模式串:_ _ _ _ _ _ _ _ _ _ _ A B A B C  

49.  比较位:                      _ ▲ ↑ OK,任务达成。  



KMP的比较位数组计算方式遵循下列公式: 

Pseud代码 

1.      next[i]= { 0 | p = 1  

2.                 k | 1 <= k < i,且 P1..Pk-1 == Pi-k+1..Pi-1  

3.                 1 | 其他情况                                }  


更详细的说明参见: 
KMP算法详解 

BM算法


首先建立两个表: 

坏字符表,包含所有可能出现的字符,0x00..0xFF,说明如果尾部字符是这个的话,模式串为了能匹配这个位置的这个字符,右移的最短距离。 

好后缀表,说明如果模式串中若有与某个长度的后缀一致的子串,则可以向右移动的距离。 

Pseud代码 

1.      对齐源串和模式串的头部;  

2.      比较模式串尾字符与源串同位置字符是否一致;  

3.        B.1 若不一致,则源串字符是否存在于模式串中;  

4.          B.1.1 若不存在,则跳过模式串长度,转到B条目继续比较;  

5.          B.1.2 若存在,则根据坏字符表右移模式串,转到B条目;  

6.        B.2 若一致,反向比较倒数第二字符是否一致,直到倒数第n字符不一致,于是……  

7.            检查坏字符表和好后缀表,选择可以移动的最大距离。  


更详细的说明参见: 
字符串匹配算法 boyer-moore算法 
Boyer-Moore 经典单模式匹配算法 

组合优化


首先,KMP算法可能会比较一些显然不可能匹配的情况: 

Pseud代码 

1.      模式串:A B A B A  

2.       next0 0 1 2 3  

3.        

4.      索引位:_ _ _ ▼ ↓  

5.      字符串:A B A B D A B ...  

6.      模式串:A B A B A  

7.      比较位:_ _ _ ▲ ↑ 失配,next[4] = 2  

8.        

9.      索引位:_ _ _ ▼ ↓  

10.  字符串:A B A B D A B ...  

11.  模式串:_ _ A B A B A  

12.  比较位:    _ ▲ ↑ 失配,next[2] = 0  

13.    

14.  索引位:_ _ _ ▼ ↓  

15.  字符串:A B A B D A B ...  

16.  模式串:_ _ _ _ A B A B A  

17.  比较位:      ▲ ↑ 失配,比较位和索引位+1进行下一个比较。  

18.    

19.  索引位:_ _ _ _ ▼ ↓  

20.  字符串:A B A B D A B ...  

21.  模式串:_ _ _ _ _ A B A B A  

22.  比较位:        ▲ ↑ OK,继续之后的比较。  



实际上,我们可以看到,上面例子里的第2,第3次比较根本没有必要。根本就可以直接跳到第4步继续下去。问题出现在什么地方?问题就出现在next的公式中。 

Pseud代码 

1.      next[i]= { 0 | p = 1  

2.                 k | 1 <= k < i,且 P1..Pk-1 == Pi-k+1..Pi-1  

3.                 1 | 其他情况                                }  


这个公式只说明了自匹配的情况,没说明在自匹配的基础上还应该有一个尾字符的不匹配。 

Pseud代码 

1.      next[i]= { 0 | p = 1  

2.                 k | 1 <= k < i,且 P1..Pk-1 == Pi-k+1..Pi-1 & Pk != Pi  

3.                 1 | 其他情况                                           }  


也就是说,若两个子串的下一个字符也一样,就没必要再匹配,因为只有源串里面比较的那个字符不一样才需要移动。 

Boyer-Moore Algorithm之中学习到,改良的BM算法中的好后缀表的建立也包含了同样的思想,而且更玄的是,好后缀表竟然能考虑到模式串的边界之外隐含的匹配信息,的确很强大。 

另外,向BM算法致敬,若尾字符根本没出现在模式串中的话,所有的比较都根本无意义。所以这种情况下,相比上面调整的KMP算法的小挪移,可以实现大挪移。 


修改之后的算法逻辑如下: 

Pseud代码 

1.      模式串:A B A B A  

2.       next0 1 0 1 0  

3.      对齐源串和模式串的头部;  

4.      比较模式串尾字符与源串同位置字符是否一致;  

5.        B.1 若不一致,则源串字符是否存在于模式串中;  

6.          B.1.1 若不存在,则跳过模式串长度,转到B条目继续比较;  

7.          B.1.2 若存在,则根据坏字符表右移模式串,转到B条目;  

8.        B.2 若一致,使用·KMP匹配,决定模式串的右移值,转到B条目继续比较。  


实际上,我在代码里创建的是跳转表,而不是指针索引位表。 

Pseud代码 

1.      模式串:A B A B A  

2.      步进值:1 1 3 3 5  表示若此位失配,则模式串向右移动的位置。  

3.        

4.      索引位:index[n] = max(n - step[n], 1);  

5.        

6.      例子1  

7.        

8.      索引位:_ _ _ ↓  

9.      字符串:A B A D A A B ...  

10.  模式串:A B A B A  

11.  比较位:_ _ _ ↑ 失配,step[4] = 3index = max(4 - step[4], 1) = 1  

12.    

13.  索引位:_ _ _ ↓  

14.  字符串:A B A D C A B ...  

15.  模式串:_ _ _ A B A B A  

16.  比较位:      ↑  

17.    

18.    

19.  例子2  

20.    

21.  索引位:_ _ ↓  

22.  字符串:A B C A B D A B ...  

23.  模式串:A B A B A  

24.  比较位:_ _ ↑ 失配,step[3] = 3index = max(3 - step[3], 1) = 1  

25.    

26.  索引位:_ _ _ ↓  

27.  字符串:A B C A B D A B ...  

28.  模式串:_ _ _ A B A B A  

29.  比较位:      ↑  

30.    

31.  例子3(更为复杂的):  

32.    

33.  模式串:A  B  A  B  A  C  A  B  A  B  A  D  

34.  步进值:1  1  3  3  5  2  7  7  9  9 11  6  

35.  索引值:1  1  1  1  1  4  1  1  1  1  1  6  

36.    

37.  索引位:_ _ _ _ _ ↓  

38.  字符串:A B A B A D A B ...  

39.  模式串:A B A B A C A B A B A D  

40.  比较位:_ _ _ _ _ ↑ 失配,step[6] = 2index = 4  

41.    

42.  索引位:_ _ _ _ _ ↓  

43.  字符串:A B A B A D A B...  

44.  模式串:_ _ A B A B A  

45.  比较位:    _ _ _ ↑  

46.    

47.  例子4  

48.    

49.  索引位:_ _ _ _ _ _ _ _ _ _ _ ↓  

50.  字符串:A B A B A C A B A B A C A B ...  

51.  模式串:A B A B A C A B A B A D  

52.  比较位:_ _ _ _ _ _ _ _ _ _ _ ↑ 失配,step[12] = 6index = 6  

53.    

54.  索引位:_ _ _ _ _ _ _ _ _ _ _ ↓  

55.  字符串:A B A B A C A B A B A C A B ...  

56.  模式串:_ _ _ _ _ _ A B A B A C A B A B A D  

57.  比较位:            _ _ _ _ _ ↑  


步进值的计算方法如下: 

Pseud代码 

1.      模式串:A  B  A  B  A  C  A  B  A  B  A D  

2.      步进值:1  2  3  4  5  6  7  8  9 10 11 12 <- 设定初始值  

3.        

4.      第一轮:设定delta = 1  

5.      从头开始比较,直到发现不同,修改发现位置的步进值,设定为min(step, delta)  

6.        

7.      模式串:A  B  A  B  A  C  A  B  A  B  A D  

8.      比较位:↑__↑  

9.      设定步进值:  

10.  步进值:1 (1) ...  

11.    

12.  第二轮:设定delta = 2  

13.  从头开始比较,直到发现不同,修改发现位置的步进值,设定为min(step, delta)  

14.  模式串:A  B  A  B  A  C  A  B  A  B  A  D  

15.  比较位:↑_____↑  

16.  比较位:   ↑_____↑  

17.  比较位:      ↑_____↑  

18.  比较位:         ↑_____↑  

19.  设定步进值:  

20.  步进值:1  1  3  4  5 (2) ...  

21.    

22.  直到delta = length - 1  

 

样例代码


最后,Java代码如下: 

Java代码 

1.      /** 

2.       * @version 1.0 

3.       * @author Regular 

4.       * @date 2009-06-11 

5.       */  

6.      package cn.sh.huang;  

7.        

8.      import java.io.File;  

9.      import java.io.FileInputStream;  

10.  import java.nio.charset.Charset;  

11.    

12.  public class StringCmp  

13.  {  

14.    

15.      /** 

16.       * Entrance method 

17.       * 

18.       * @param args 

19.       */  

20.      public static void main(String[] args) throws Exception  

21.      {  

22.          String fileName = "C://Program Files//Java//jdk1.6.0_13//LICENSE";  

23.          String pattern = "enef";  

24.          File file = new File(fileName);  

25.          int fileLen = (int) file.length();  

26.          FileInputStream fis = new FileInputStream(file);  

27.          byte[] buffer = new byte[fileLen];  

28.          fis.read(buffer);  

29.          int i = indexOfData(buffer, 0, pattern);  

30.          System.out.println(i);  

31.      }  

32.    

33.      private static int indexOfData(byte[] buffer, int index, String s)  

34.      {  

35.          byte[] pattern = s.getBytes(Charset.forName("US-ASCII"));  

36.          int[] fast_shift = new int[256]; // 模式串尾字符比较结果的移动  

37.          for (int i = 0; i < 256; i++) {  

38.              fast_shift[i] = pattern.length;  

39.          }  

40.          for (int i = pattern.length - 1, j = 0; i >= 0; i--, j++) {  

41.              int x = 0xFF & pattern[i];  

42.              if (fast_shift[x] > j) {  

43.                  fast_shift[x] = j;  

44.              }  

45.          }  

46.          int[] slow_shift = new int[pattern.length]; // ·KMP算法的移动  

47.          getNextStep(pattern, slow_shift);  

48.          int cursor = 0;  

49.          outterLoop: while (index + pattern.length <= buffer.length) {  

50.              // 首先检查index + pattern.length - 1位置的字符,决定快速移动距离  

51.              int x = 0xFF & buffer[index + pattern.length - 1];  

52.              int shift = fast_shift[x];  

53.              if (shift > 0) {  

54.                  index += shift;  

55.                  cursor = 0;  

56.                  continue;  

57.              }  

58.              // 若尾字符一致,使用改·KMP算法决定慢速移动距离  

59.              while (cursor < pattern.length - 1) {  

60.                  if (pattern[cursor] != buffer[index + cursor]) {  

61.                      index += slow_shift[cursor];  

62.                      cursor = cursor - slow_shift[cursor];  

63.                      if (cursor < 0) {  

64.                          cursor = 0;  

65.                      }  

66.                      continue outterLoop;  

67.                  }  

68.                  cursor++;  

69.              }  

70.              return index;  

71.          }  

72.          return -1;  

73.      }  

74.    

75.      /** 

76.       * <pre> 

77.       *                   idx = max(0, n - step) 

78.       * a b a b a c a b a b a d                        step    idx 

79.       * X a b a b a c a b a b a d                       1       0 

80.       * a X b a b a c a b a b a d                       1       0 

81.       * a b X a b a b a c a b a b a d                   3       0 

82.       * a b a X b a b a c a b a b a d                   3       0 

83.       * a b a b X a b a b a c a b a b a d               5       0 

84.       * a b a b a X a c a b a b a d                     2       3 

85.       * a b a b a c X a b a b a c a b a b a d           7       0 

86.       * a b a b a c a X b a b a c a b a b a d           7       0 

87.       * a b a b a c a b X a b a b a c a b a b a d       9       0 

88.       * a b a b a c a b a X b a b a c a b a b a d       9       0 

89.       * a b a b a c a b a b X a b a b a c a b a b a d  11       0 

90.       * a b a b a c a b a b a X a b a b a d             6       5 

91.       * </pre> 

92.       * @param pattern 

93.       * @param next 

94.       */  

95.      private static void getNextStep(byte[] pattern, int[] next)  

96.      {  

97.          for (int i = 0; i < pattern.length; i++) {  

98.              next[i] = i + 1;  

99.          }  

100.           // 卷积  

101.           for (int delta = 1; delta < pattern.length; delta++) {  

102.               int i = 0;  

103.               int j = i + delta;  

104.               while (pattern.length > j && pattern[i] == pattern[j]) {  

105.                   i++;  

106.                   j++;  

107.               }  

108.               if (pattern.length > j) {  

109.                   if (next[j] > delta) {  

110.                       next[j] = delta;  

111.                   }  

112.               }  

113.           }  

114.       }  

115.     

116.   }  



总体看来,还是BM算法更好一些,其效率比KMP算法更高。 

我这里虽然借用了BM的坏字符思想并改进了KMP的步进值计算,但由于逐字符比较始终是从头开始,而不像BM算法是从尾部开始,所以小挪移的潜力没有BM的那么大。 

后续设想


若模式串相对较长的话,可以在模式串中找几个稀疏分布的点,比较的时候,首先比较这几个点的字符是否与源串相同,如果不同就没必要逐个字符比较了,肯定不一致,可以往后挪了。

 

原创粉丝点击