对于经典模式匹配算法的一些更改
对于经典模式匹配算法的一些改动
从一个很长的字符串(或者数组)中,查找某个子串(模式串)是否存在,在算法上被称为是“模式匹配”。
模式匹配的经典算法包括KMP算法、BM算法等等。以下简要回顾这些经典算法的思想,并说明我对此的改进想法。
首先对模式串进行处理,获得当某个字符位置失配的时候,比较位置的指针应该指向的下一个位置的数组。举例如下:
经过对上面的模式串进行处理,我们可以做如下比较:
KMP的比较位数组计算方式遵循下列公式:
更详细的说明参见:
KMP算法详解
首先建立两个表:
坏字符表,包含所有可能出现的字符,0x00..0xFF,说明如果尾部字符是这个的话,模式串为了能匹配这个位置的这个字符,右移的最短距离。
好后缀表,说明如果模式串中若有与某个长度的后缀一致的子串,则可以向右移动的距离。
更详细的说明参见:
字符串匹配算法 boyer-moore算法
Boyer-Moore 经典单模式匹配算法
首先,KMP算法可能会比较一些显然不可能匹配的情况:
实际上,我们可以看到,上面例子里的第2,第3次比较根本没有必要。根本就可以直接跳到第4步继续下去。问题出现在什么地方?问题就出现在next的公式中。
这个公式只说明了自匹配的情况,没说明在自匹配的基础上还应该有一个尾字符的不匹配。
也就是说,若两个子串的下一个字符也一样,就没必要再匹配,因为只有源串里面比较的那个字符不一样才需要移动。
从Boyer-Moore Algorithm之中学习到,改良的BM算法中的好后缀表的建立也包含了同样的思想,而且更玄的是,好后缀表竟然能考虑到模式串的边界之外隐含的匹配信息,的确很强大。
另外,向BM算法致敬,若尾字符根本没出现在模式串中的话,所有的比较都根本无意义。所以这种情况下,相比上面调整的KMP算法的小挪移,可以实现大挪移。
修改之后的算法逻辑如下:
实际上,我在代码里创建的是跳转表,而不是指针索引位表。
步进值的计算方法如下:
最后,Java代码如下:
总体看来,还是BM算法更好一些,其效率比KMP算法更高。
我这里虽然借用了BM的坏字符思想并改进了KMP的步进值计算,但由于逐字符比较始终是从头开始,而不像BM算法是从尾部开始,所以小挪移的潜力没有BM的那么大。
若模式串相对较长的话,可以在模式串中找几个稀疏分布的点,比较的时候,首先比较这几个点的字符是否与源串相同,如果不同就没必要逐个字符比较了,肯定不一致,可以往后挪了。
从一个很长的字符串(或者数组)中,查找某个子串(模式串)是否存在,在算法上被称为是“模式匹配”。
模式匹配的经典算法包括KMP算法、BM算法等等。以下简要回顾这些经典算法的思想,并说明我对此的改进想法。
KMP算法
首先对模式串进行处理,获得当某个字符位置失配的时候,比较位置的指针应该指向的下一个位置的数组。举例如下:
模式串:A B A B C next :0 0 1 2 0
经过对上面的模式串进行处理,我们可以做如下比较:
索引位:_ _ ▼ ↓ 字符串:A B A A B A B D C A B A B A B C 模式串:A B A B C 比较位:_ _ ▲ ↑ 失配,指针指向 next[3] = 1 的位置 索引位:_ _ ▼ ↓ 字符串:A B A A B A B D C A B A B A B C 模式串:_ _ A B A B C 比较位: ▲ ↑ 仍然失配,next[1] = 0 的位置 索引位:_ _ ▼ ↓ 字符串:A B A A B A B D C A B A B A B C 模式串:_ _ _ A B A B C 比较位: ▲ ↑ OK了,继续向下走。 索引位:_ _ _ _ _ _ ▼ ↓ 字符串:A B A A B A B D C A B A B A B C 模式串:_ _ _ A B A B C 比较位: _ _ _ ▲ ↑ 失配,next[4] = 2 的位置。 索引位:_ _ _ _ _ _ ▼ ↓ 字符串:A B A A B A B D C A B A B A B C 模式串:_ _ _ _ _ A B A B C 比较位: _ ▲ ↑ 仍然失配,next[2] = 0 的位置。 索引位:_ _ _ _ _ _ ▼ ↓ 字符串:A B A A B A B D C A B A B A B C 模式串:_ _ _ _ _ _ _ A B A B C 比较位: ▲ ↑ 仍然失配且比较位已经是0了,只能索引,比较+1继续找。 索引位:_ _ _ _ _ _ _ ▼ ↓ 字符串:A B A A B A B D C A B A B A B C 模式串:_ _ _ _ _ _ _ _ A B A B C 比较位: ▲ ↑ 仍然失配且比较位已经是0了,只能索引,比较+1继续找。 索引位:_ _ _ _ _ _ _ _ ▼ ↓ 字符串:A B A A B A B D C A B A B A B C 模式串:_ _ _ _ _ _ _ _ _ A B A B C 比较位: ▲ ↑ OK,继续向下走。 索引位:_ _ _ _ _ _ _ _ _ _ _ _ ▼ ↓ 字符串:A B A A B A B D C A B A B A B C 模式串:_ _ _ _ _ _ _ _ _ A B A B C 比较位: _ _ _ ▲ ↑ 失配,next[4] = 2 的位置。 索引位:_ _ _ _ _ _ _ _ _ _ _ _ ▼ ↓ 字符串:A B A A B A B D C A B A B A B C 模式串:_ _ _ _ _ _ _ _ _ _ _ A B A B C 比较位: _ ▲ ↑ OK,任务达成。
KMP的比较位数组计算方式遵循下列公式:
next[i]= { 0 | p = 1 k | 1 <= k < i,且 P1..Pk-1 == Pi-k+1..Pi-1 1 | 其他情况 }
更详细的说明参见:
KMP算法详解
BM算法
首先建立两个表:
坏字符表,包含所有可能出现的字符,0x00..0xFF,说明如果尾部字符是这个的话,模式串为了能匹配这个位置的这个字符,右移的最短距离。
好后缀表,说明如果模式串中若有与某个长度的后缀一致的子串,则可以向右移动的距离。
A 对齐源串和模式串的头部; B 比较模式串尾字符与源串同位置字符是否一致; B.1 若不一致,则源串字符是否存在于模式串中; B.1.1 若不存在,则跳过模式串长度,转到B条目继续比较; B.1.2 若存在,则根据坏字符表右移模式串,转到B条目; B.2 若一致,反向比较倒数第二字符是否一致,直到倒数第n字符不一致,于是…… 检查坏字符表和好后缀表,选择可以移动的最大距离。
更详细的说明参见:
字符串匹配算法 boyer-moore算法
Boyer-Moore 经典单模式匹配算法
组合优化
首先,KMP算法可能会比较一些显然不可能匹配的情况:
模式串:A B A B A next:0 0 1 2 3 索引位:_ _ _ ▼ ↓ 字符串:A B A B D A B ... 模式串:A B A B A 比较位:_ _ _ ▲ ↑ 失配,next[4] = 2。 索引位:_ _ _ ▼ ↓ 字符串:A B A B D A B ... 模式串:_ _ A B A B A 比较位: _ ▲ ↑ 失配,next[2] = 0。 索引位:_ _ _ ▼ ↓ 字符串:A B A B D A B ... 模式串:_ _ _ _ A B A B A 比较位: ▲ ↑ 失配,比较位和索引位+1进行下一个比较。 索引位:_ _ _ _ ▼ ↓ 字符串:A B A B D A B ... 模式串:_ _ _ _ _ A B A B A 比较位: ▲ ↑ OK,继续之后的比较。
实际上,我们可以看到,上面例子里的第2,第3次比较根本没有必要。根本就可以直接跳到第4步继续下去。问题出现在什么地方?问题就出现在next的公式中。
next[i]= { 0 | p = 1 k | 1 <= k < i,且 P1..Pk-1 == Pi-k+1..Pi-1 1 | 其他情况 }
这个公式只说明了自匹配的情况,没说明在自匹配的基础上还应该有一个尾字符的不匹配。
next[i]= { 0 | p = 1 k | 1 <= k < i,且 P1..Pk-1 == Pi-k+1..Pi-1 & Pk != Pi 1 | 其他情况 }
也就是说,若两个子串的下一个字符也一样,就没必要再匹配,因为只有源串里面比较的那个字符不一样才需要移动。
从Boyer-Moore Algorithm之中学习到,改良的BM算法中的好后缀表的建立也包含了同样的思想,而且更玄的是,好后缀表竟然能考虑到模式串的边界之外隐含的匹配信息,的确很强大。
另外,向BM算法致敬,若尾字符根本没出现在模式串中的话,所有的比较都根本无意义。所以这种情况下,相比上面调整的KMP算法的小挪移,可以实现大挪移。
修改之后的算法逻辑如下:
模式串:A B A B A next:0 1 0 1 0 A 对齐源串和模式串的头部; B 比较模式串尾字符与源串同位置字符是否一致; B.1 若不一致,则源串字符是否存在于模式串中; B.1.1 若不存在,则跳过模式串长度,转到B条目继续比较; B.1.2 若存在,则根据坏字符表右移模式串,转到B条目; B.2 若一致,使用“改·KMP法”匹配,决定模式串的右移值,转到B条目继续比较。
实际上,我在代码里创建的是跳转表,而不是指针索引位表。
模式串:A B A B A 步进值:1 1 3 3 5 表示若此位失配,则模式串向右移动的位置。 索引位:index[n] = max(n - step[n], 1); 例子1: 索引位:_ _ _ ↓ 字符串:A B A D A A B ... 模式串:A B A B A 比较位:_ _ _ ↑ 失配,step[4] = 3,index = max(4 - step[4], 1) = 1。 索引位:_ _ _ ↓ 字符串:A B A D C A B ... 模式串:_ _ _ A B A B A 比较位: ↑ 例子2: 索引位:_ _ ↓ 字符串:A B C A B D A B ... 模式串:A B A B A 比较位:_ _ ↑ 失配,step[3] = 3,index = max(3 - step[3], 1) = 1。 索引位:_ _ _ ↓ 字符串:A B C A B D A B ... 模式串:_ _ _ A B A B A 比较位: ↑ 例子3(更为复杂的): 模式串:A B A B A C A B A B A D 步进值:1 1 3 3 5 2 7 7 9 9 11 6 索引值:1 1 1 1 1 4 1 1 1 1 1 6 索引位:_ _ _ _ _ ↓ 字符串:A B A B A D A B ... 模式串:A B A B A C A B A B A D 比较位:_ _ _ _ _ ↑ 失配,step[6] = 2,index = 4。 索引位:_ _ _ _ _ ↓ 字符串:A B A B A D A B... 模式串:_ _ A B A B A 比较位: _ _ _ ↑ 例子4: 索引位:_ _ _ _ _ _ _ _ _ _ _ ↓ 字符串:A B A B A C A B A B A C A B ... 模式串:A B A B A C A B A B A D 比较位:_ _ _ _ _ _ _ _ _ _ _ ↑ 失配,step[12] = 6,index = 6。 索引位:_ _ _ _ _ _ _ _ _ _ _ ↓ 字符串:A B A B A C A B A B A C A B ... 模式串:_ _ _ _ _ _ A B A B A C A B A B A D 比较位: _ _ _ _ _ ↑
步进值的计算方法如下:
模式串:A B A B A C A B A B A D 步进值:1 2 3 4 5 6 7 8 9 10 11 12 <- 设定初始值 第一轮:设定delta = 1 从头开始比较,直到发现不同,修改发现位置的步进值,设定为min(step, delta) 模式串:A B A B A C A B A B A D 比较位:↑__↑ 设定步进值: 步进值:1 (1) ... 第二轮:设定delta = 2 从头开始比较,直到发现不同,修改发现位置的步进值,设定为min(step, delta) 模式串:A B A B A C A B A B A D 比较位:↑_____↑ 比较位: ↑_____↑ 比较位: ↑_____↑ 比较位: ↑_____↑ 设定步进值: 步进值:1 1 3 4 5 (2) ... 直到delta = length - 1
样例代码
最后,Java代码如下:
/** * @version 1.0 * @author Regular * @date 2009-06-11 */ package cn.sh.huang; import java.io.File; import java.io.FileInputStream; import java.nio.charset.Charset; public class StringCmp { /** * Entrance method * * @param args */ public static void main(String[] args) throws Exception { String fileName = "C:\\Program Files\\Java\\jdk1.6.0_13\\LICENSE"; String pattern = "enef"; File file = new File(fileName); int fileLen = (int) file.length(); FileInputStream fis = new FileInputStream(file); byte[] buffer = new byte[fileLen]; fis.read(buffer); int i = indexOfData(buffer, 0, pattern); System.out.println(i); } private static int indexOfData(byte[] buffer, int index, String s) { byte[] pattern = s.getBytes(Charset.forName("US-ASCII")); int[] fast_shift = new int[256]; // 模式串尾字符比较结果的移动 for (int i = 0; i < 256; i++) { fast_shift[i] = pattern.length; } for (int i = pattern.length - 1, j = 0; i >= 0; i--, j++) { int x = 0xFF & pattern[i]; if (fast_shift[x] > j) { fast_shift[x] = j; } } int[] slow_shift = new int[pattern.length]; // 改·KMP算法的移动 getNextStep(pattern, slow_shift); int cursor = 0; outterLoop: while (index + pattern.length <= buffer.length) { // 首先检查index + pattern.length - 1位置的字符,决定快速移动距离 int x = 0xFF & buffer[index + pattern.length - 1]; int shift = fast_shift[x]; if (shift > 0) { index += shift; cursor = 0; continue; } // 若尾字符一致,使用改·KMP算法决定慢速移动距离 while (cursor < pattern.length - 1) { if (pattern[cursor] != buffer[index + cursor]) { index += slow_shift[cursor]; cursor = cursor - slow_shift[cursor]; if (cursor < 0) { cursor = 0; } continue outterLoop; } cursor++; } return index; } return -1; } /** * <pre> * idx = max(0, n - step) * a b a b a c a b a b a d step idx * X a b a b a c a b a b a d 1 0 * a X b a b a c a b a b a d 1 0 * a b X a b a b a c a b a b a d 3 0 * a b a X b a b a c a b a b a d 3 0 * a b a b X a b a b a c a b a b a d 5 0 * a b a b a X a c a b a b a d 2 3 * a b a b a c X a b a b a c a b a b a d 7 0 * a b a b a c a X b a b a c a b a b a d 7 0 * a b a b a c a b X a b a b a c a b a b a d 9 0 * a b a b a c a b a X b a b a c a b a b a d 9 0 * 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 * a b a b a c a b a b a X a b a b a d 6 5 * </pre> * @param pattern * @param next */ private static void getNextStep(byte[] pattern, int[] next) { for (int i = 0; i < pattern.length; i++) { next[i] = i + 1; } // 卷积 for (int delta = 1; delta < pattern.length; delta++) { int i = 0; int j = i + delta; while (pattern.length > j && pattern[i] == pattern[j]) { i++; j++; } if (pattern.length > j) { if (next[j] > delta) { next[j] = delta; } } } } }
总体看来,还是BM算法更好一些,其效率比KMP算法更高。
我这里虽然借用了BM的坏字符思想并改进了KMP的步进值计算,但由于逐字符比较始终是从头开始,而不像BM算法是从尾部开始,所以小挪移的潜力没有BM的那么大。
后续设想
若模式串相对较长的话,可以在模式串中找几个稀疏分布的点,比较的时候,首先比较这几个点的字符是否与源串相同,如果不同就没必要逐个字符比较了,肯定不一致,可以往后挪了。
1 楼
lin_style
2009-06-12
重温,兼收藏。
2 楼
iamsk
2009-06-16
收藏,有空定会细看的。