字符串匹配(string match)是在实际工程中经常会碰到的问题,通常其输入是原字符串(String)和子串(又称模式,Pattern)组成,输出为子串在原字符串中的首次出现的位置。通常精确的字符串搜索算法包括暴力搜索(Brute force),KMP, BM(Boyer Moore), sunday, robin-karp 以及 bitap。下面分析这几种方法并给出其实现。假设原字符串长度M,字串长度为N。
1. Brute force.
该方法又称暴力搜索,也是最容易想到的方法。
预处理时间 O(0)
匹配时间复杂度O(N*M)
主要过程:从原字符串开始搜索,若出现不能匹配,则从原搜索位置+1继续。
2,KMP.
KMP是经典的字符串匹配算法。
预处理时间:O(M)
匹配时间复杂度:O(N)
主要过程:通过对字串进行预处理,当发现不能匹配时,可以不进行回溯。
注意:在预处理中,表面看起来时间复杂度为O(N^2),但是为什么是线性的,在时间复杂度分析中中,通过观察变量的变化来统计零碎的、执行次数不规则的情况,这种方法叫做摊还分析。我们从上述程序的j 值入手。每一次执行上述循环预处理语句中的第二个else时都会使j减小(但不能减成负的),而另外的改变j值的地方只有一处。每次执行了这一处,j都只能加1;因此,整个过程中j最多加了M-1个1。于是,j最多只有M-1次减小的机会(j值减小的次数当然不能超过M-1,因为j永远是非负整数)。这告诉我们,while循环总共最多执行了M-1次。按照摊还分析的说法,平摊到每次for循环中后,一次for循环的复杂度为O(1)。整个过程显然是O(M)的。另外关于KMP的详细分析,可以参考Matrix67KMP算法详解。
3,Boyer Moore
Boyer Moore是字符串匹配算法中的经典,可以参考论文a faster string searching algorithm。
预处理时间O(N + M^2)
匹配时间复杂度O(N)
主要过程:通过预处理原字符串以及待匹配字串,从而在匹配失败时可以跳过更多的字符。
提示:该算法主要利用坏字符规则和好后缀规则进行转换。所谓坏字符规则,是指不能匹配时的字符在待匹配字串中从右边数的位置;而好后缀规则则是指子串中从该不匹配位置后面所有字符(都是已匹配字符)再次在字串中出现的位置(k),其中s[k,k+1,---,k+len-j-1] = s[j+1, j+1,---,len-1], 并且s[k-1] != [j] || s[k-1] = $, 其中$表示增补的字符,可以与任何字符相等。
举例来说,对于字串ABCXXXABC
-4 -3 -2 -1 0 1 2 3 4 5 6 7 8 9
A B C X X X A B C
j=9 9//NULL->其值为当前位置。
j=8 $ 0 //C->虽然出现在3,但[2] = [j],所以不满足
j=7 $ $ -1 //BC出现在开始[2],但[1]=[j]
j=6 1 //ABC
j=5 $ 0 //XABC
j=4 $ $ -1 //XXABC
j=3 $ $ $ -2 //XXXABC
j=2 $ $ $ $ -3 //CXXXABC
j=1 $ $ $ $ $ -4 //BCXXXABC
4, Sunday
Sunday算法比较简单,其实就是利用Boyer Moore中的坏字符规则,实现起来简单,效果也还不错。
预处理时间O(M)
匹配时间复杂度O(N*M)
5, Robin-Karp
Robin-Karp主要利用HASH函数来处理字串,从而完成匹配。
预处理时间O(0)
最坏匹配时间复杂度O(N*M)
注意:主要依赖于hash函数的设计。
6, Bitap
Bitap算法主要利用位运算进行字符串的匹配,其匹配过程可以看作是有穷自动机中状态的转换,按照字串(pattern)的连续分解状态进行转换,从而到达终点,此时匹配过程完成。
预处理时间O(M)
最坏匹配时间复杂度O(N*M)
注意:Bitap匹配算法中可以改用位移操作实现,从而将匹配复杂度从O(N*M)降低到O(N)。
总结,以上算法中,性能较好的为KMP,BM, 实现简单的为BF,Sunday,Bitap。两者折中来看,KMP表现较好。
预处理时间 匹配时间复杂度
BF O(0) O(N*M)
KMP O(M) O(N)
BM O(N+M^2) O(N)
Sunday O(M) O(N*M)
Robin-Karp O(0) O(N*M)
Bitap O(M) O(N*M)->O(N)
以上六种算法比较实现的代码如下所示(其中string长度10000)。