英语里有句习语叫"find a needle in a haystack",译成中文叫"大海捞针"(原意是在一堆干草中寻找一根针)。计算机中的文本匹配(string matching)就是要解决怎样在一段很长的文本中找到符合要求的一个子串,该子串通常叫模式串(pattern),也就是对应我们要找的“针”。常用的文本精确匹配(exact string matching)算法有蛮力法(brute-force),Boyer-Moore算法和KMP算法。后两种算法都是用空间换 时间的经典案例,不像蛮力法,文本的位置指针i不需要回退到已经匹配过的字符位置(BM算法的每次匹配虽然从右向左移动指针,但从来不会将i指针回退到已经匹配过的字符位置)。在最坏情况下的时间复杂度是与文本长度成线性关系的。特别需要说明的是,BM算法和本文中介绍的它的简化版本Horspool算 法,在最佳情况下的时间复杂度是O(n/m),因此对模式串比较长的情况(比如长度大于5)应用该算法比较有效。
本文介绍Boyer-Moore算法的一个简化版本,叫做Boyer–Moore–Horspool算法或直接叫做Horspool算法。
设文本T长度为n,模式串P长度为m。T的当前位置指针为i(0<=i<n),P的当前位置指针为j(0<=j<m)。
算法:
基本思想是模式串窗口沿着文本向右滑动,但是每次滑动的距离不能太大,否则会冒着遗漏可能成功匹配的字符串的风险。在模式串窗口滑动完成后,开始重新设置i和j指针,并且让i和j分别从右到左挨个字符匹配。匹配到某个字符位置时发现T[i]和P[j]不相同了(即发现了mismatch,字符T[i]也叫做bad character),就要分以下三种情况讨论:
1)如果T[i]不在模式串中存在,需要将模式串窗口沿着文本向右滑动j+1个字符位置。这是因为j前面的任何字符P[0..j-1]都与T[i]不匹配,可以直接将P[0]跟i的下一个字符T[i+1]对齐用作下一次的匹配。
例如:
此时j=3,T[i]=T,P[j]=D,但是字符T[i]=T在模式串中没有出现。直接将P[0]=N跟i的下一个字符T [i+1]=L对齐用作下一次的匹配。所以下一次滑动窗口的位置如上所示。
2)如果T[i]在模式串中存在,分两种情况讨论:
case 2a) 设P中最右边出现的字符T[i]所处的位置为q,但是q>j,这时如果还要让T[i]跟P[q]对齐的话,势必要让模式串窗口沿着文本向左移动,显然这是做无用功。所以正确而且最保守的做法是将模式串窗口沿着文本向右移动1个字符位置。
例如:
此时j=4,T[i]=E,P[j]=L,这里字符T[i]=E在模式串中最右边位置q=5(注意必须是最右边位置,左边的两个E不算),因为q=5>j=4,所以最保守的情况是将P只向右滑动一个字符位置,如上图所示。
case 2b)如果q<j (显然q不可能等于j了,因为T[i] != P[j]),这是就需要将T[i]跟P[q]对齐,此时能保证模式串窗口会向右滑动。模式窗口向右滑动的字符个数为j-q。
例如:
此时j=4,T[i]=D,P[j]=L,这里字符T[i]=D在模式串中最右边位置q=3,因为q=3<=j=4,所以将模式窗 口向右滑动j-q=1个字符以保持T[i]=D和j左边的第一个字符D对齐,如上图所示。
注意在下一次匹配时,需要调整j到模式串的末尾字符位置,并且i和j对齐。可用公式得出:
i <- i+m-min(j,1+q)
j <- m -1
二者顺序不能颠倒,因为计算i时需要j的原来值。
根据上面的讨论前提是需要知道q的值,即解决这样一个问题:某个字符c(即bad character)在模式串P中最右边出现的位置是多少?考虑到文本中这样的c可能为任意字符,需要预先计算一张表(叫做bad- character shift表, 表的索引值范围即字符集的所有字符值。具体实现时,如果c在模式串中不出现, 该表对应预设值-1。参考下面的实现。
算法复杂度:Horspool算法在平均情况下的时间复杂度是O(n),但是在最坏情况下的复杂度跟蛮力法在同一个数量级,即O(mn),非简化版本的BM算法在最坏情况下的复杂度是线性O(n)。本博客另外一篇文章将介绍原始的BM算法(BM算法是1977年发明的,而Horspool算法是1980提出来的)如何在最坏的情况下复杂度跟KMP算法同级即O(n), 。
最坏情况:
例如文本中只含一个字母:T=AAAAAAAAA...AA, 模式串P=BAAA。应用在本算法中的case 2a每次只将模式串沿文本向右滑动一个字符。这跟蛮力法的操作次数一样多。
最佳情况:
文本T=After a long text, here's a needle ZZZZZ
模式串P=ZZZZZ
匹配的过程如下(注意每次i和j指针跳跃距离都是模式串长度m):
实现:
import java.util.Arrays;/** * * @author ljs * 2011-06-19 * * 版权说明:可以复制,但须标明出处 * * Assuming we use ASCII charset * */public class Horspool {private static final int CHARSET_SIZE = 256;//prepare the shift table: find the rightmost position //in the patternprivate int[] makeShiftTable(String pattern){int[] shiftTable = new int[CHARSET_SIZE];//init to -1for(int i=0;i<CHARSET_SIZE;i++)shiftTable[i]=-1;//set the pattern charsfor(int i=0;i<pattern.length();i++){char c = pattern.charAt(i);//OK: the rightmost position i may overwrite the left positionsshiftTable[c] = i;}return shiftTable;}//return the first matched substring's position;//return -1 if no matchpublic int match(String text,String pattern){int n = text.length();int m = pattern.length();if(m>n) return -1;int[] shiftTable = makeShiftTable(pattern);/****BEGIN TEST: the following code snippet can be commented out****/System.out.format("%s%n",text);System.out.format("%s%n",pattern);/****END TEST: the above code snippet can be commented out****/int i = m -1;int j = m -1;do{int c = text.charAt(i);if(c == pattern.charAt(j)){if(j==0){//find a matchreturn i;}else{//BM algorithm: move from right to lefti--;j--;}}else{/****BEGIN TEST: the following code snippet can be commented out****/int q = shiftTable[c];int dotsCount = 0;if(q == -1){//case 1dotsCount = i+1;}else if(q>j){ //case 2adotsCount = i-j+1;}else{ //case 2bdotsCount = i-q;}byte dot[] = new byte[dotsCount]; Arrays.fill(dot, (byte)'.');System.out.format("%s%s%n",new String(dot),pattern);/****END TEST: the above code snippet can be commented out****///determine the i and j for next match attemptint p = shiftTable[c] + 1;if(j<=p){i += m - j;}else{i += m - p;}j = m - 1;}}while(i<=n-1);//no matchreturn -1;}public static void findMatch(Horspool solver,String text,String pattern){int index = solver.match(text, pattern);if(index>=0){System.out.format("Found at position %d%n",index);}else{System.out.format("No match%n");}}public static void main(String[] args) {Horspool solver = new Horspool();String text = "A SLOW TURTLE";String pattern = "NEEDLE";Horspool.findMatch(solver,text,pattern);System.out.format("**************%n");text = "ABCELE";pattern = "NABDLE";Horspool.findMatch(solver,text,pattern);System.out.format("**************%n");text = "After a long text, here's a needle ZZZZZ";pattern = "ZZZZZ";Horspool.findMatch(solver,text,pattern);System.out.format("**************%n");text = "The quick brown fox jumps over the lazy dog.";pattern = "lazy";Horspool.findMatch(solver,text,pattern);System.out.format("**************%n");text = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna...";pattern = "tempor";Horspool.findMatch(solver,text,pattern);}}
测试结果:
A SLOW TURTLENEEDLE......NEEDLE.......NEEDLE...........NEEDLENo match**************ABCELENABDLE.NABDLENo match**************After a long text, here's a needle ZZZZZZZZZZ.....ZZZZZ..........ZZZZZ...............ZZZZZ....................ZZZZZ.........................ZZZZZ..............................ZZZZZ...................................ZZZZZFound at position 35**************The quick brown fox jumps over the lazy dog.lazy....lazy........lazy............lazy................lazy....................lazy........................lazy............................lazy................................lazy...................................lazyFound at position 35**************Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna...tempor......tempor............tempor..................tempor.....................tempor...........................tempor...............................tempor....................................tempor..........................................tempor................................................tempor......................................................tempor..........................................................tempor...........................................................tempor.................................................................tempor..................................................................tempor........................................................................tempor.........................................................................temporFound at position 73