子串检索有着很广泛的应用,例如在文档软件中查找关键词,网站过滤敏感词,生物学家查找某种模式的基因组序列等等,很多人听说过著名的KMP算法,Boyer-Moore算法做到的更多,有迹象表明在某些情况下效率是前者的3-5倍,且实现起来更加简单,符合我简单高效的原则。
下面先抛开算法不谈,如果让你在ABCSAKDFFEHHJDDEFKLD
中查找DDEFK
,你会怎么做?
1 2 |
ABCSAKDFFEHHJDDEFKLD DDEFK |
最直接的就是暴力检索法,挨个比较文本和模式的每个字符,成功就继续比较模式字符的下一个,否则将模式往右移动一位,继续上述过程,直到文本的结尾或者搜索成功,通常情况下效率还可以,因为对于大部分文档往往只需要比较模式中的一两个字符,就会有非匹配字符,因此模式可以快速的向右移动,整体运行时间接近线性。java示例代码为
1 2 3 4 5 6 7 8 9 10 11 12 13 |
int M = pat.length(); int N = txt.length(); for(int i = 0; i <= N - M; i++) { for (int j = 0; j < M; j++) { if (txt.charAt(i + j) != pat.charAt(j)) { break; } if (j == M - 1) { return i; } } } return -1; |
而本文的主角BM算法可谓别出心裁,它从后往前匹配模式的每一个字符,看看BM算法是如何处理上面的例子的,我们用i表示文本的起始位置,j表示模式中待匹配字符的位置。
第一步,i=0,j=4,A与K匹配失败,没有必要再往前匹配,i往后移动4+1=5个字符,因为小于这个数字,A都会与模式中的某个字符重叠,而模式中没有这个字符,无论如何都会失败。
1 2 3 4 |
i=0 ABCSAKDFFEFKJDDEFKLD DDEFK j=4 |
第二步,i=5,j=4,E与K匹配失败,i需要再次往后移动,这次需要移动几个字符呢,答案是2,这样会将模式中最右边的E与文本中E对齐,小于这个数,文本中E会与模式E右边的字符重叠,这些字符中没有E,因此不可能成功。
1 2 3 4 |
i=5 ABCSAKDFFEFKJDDEFKLD DDEFK j=4 |
第三步,i=7,j=4,这次匹配成功了,j减一j=3,又成功了,j再减一j=2,又成功了,j再减一j=1,这次F与D没有匹配成功,这次i要移动多少呢,F在文本和模式中都出现了,但是模式中的F已经匹配过了,我们不想让i回退,只能让i简单的加1。
1 2 3 4 5 6 7 |
i=7 ABCSAKDFFEFKJDDEFKLD DDEFK j=4 j=3 j=2 j=1 |
第四步,i=8,j=4,同样J和K匹配失败,且J不在模式字符串中,同第一步,我们将i移动4+1=5个字符。
1 2 3 4 |
i=8 ABCSAKDFFEFKJDDEFKLD DDEFK j=4 |
第五步,i=13,k=4,当j=4…0时,每个字符都匹配成功,成功检索到模式,将i=13返回,或者将i的值存储起来继续往后搜索,如果想得到模式的所有位置。
1 2 3 4 5 6 7 8 |
i=13 ABCSAKDFFEFKJDDEFKLD DDEFK j=4 j=3 j=2 j=1 j=0 |
这样i移动5次,总共比较了12个字符,就完成了查找。
总结一下,BM算法的策略是从后往前匹配模式中的每个字符,直到文本中出现一个不匹配的字符txt.charAt(i+j)
或者检索成功返回i。与暴力检索不同的是,当匹配失败时,BM算法不会按部就班的移动i,它首先会构造一个right数组,数组中存储的是字符集中每个字符在模式中最右边的位置,如果字符不在模式中设为-1,比如上面的例子,