字符串匹配基础
字符串匹配算法有很多,首先讲一下BF算法和RK算法。RK算法是BF算法的改进,借助了前面讲的哈希算法来实现高效字符串匹配。
BF算法
BF算法是Brute Force的缩写,中文叫作暴力匹配法,也叫朴素匹配算法。首先要了解主串和模式串的概念。比如我们在字符串A中查找字符串B,则A为主串,B为模式串。若主串长度为n,模式串长度为m,则n>m。
BF算法作为最简单、最暴力的字符串匹配算法。在主串中检查起始位置分别是0、1、2、…、n-m且长度为m的n-m+1个子串,看是否有和模式串匹配。
最坏情况下,要比对n-m+1次,每次要比对m个字符。故最坏情况时间复杂度为O( n ∗ m n*m n∗m)。
尽管时间复杂度很高,但在实际开发中,它比较常用,因:
(1)实际开发中,主串和模式串都不会太长。匹配时,若中途遇到不能匹配的字符,就可以停止了。在大部分情况下,算法执行效率比O( n ∗ m n*m n∗m)要高得多。
(2)该匹配算法思想简单,代码实现也很简单。
def BF(a,b): #a为主串,b为模式串
n = len(a)
m = len(b)
for i in range(n-m+1):
for j in range(m):
if a[i+j]!=b[j]:
break
if j == m-1:
return i
if i == n-m+1:return -1
RK算法
在BF算法的基础上引入了哈希算法:通过哈希算法对主串中的n-m+1个子串分别求哈希值,然后逐个与模式串的哈希值比较大小。如果某个子串的哈希值与模式串相等,那就说明相应的子串和模式串匹配。因为哈希值是一数字,判断数字之间比较是否相等是非常快速的,因此比较效率就提高了。
不过提高哈希算法计算子串的哈希值时,需遍历子串中的每个字符。尽管比较的效率提高了,算法的整体效率并没有提高。
如何提高哈希算法计算子串哈希值的效率?这就需要哈希算法设计的有技巧。假设要匹配的字符串的字符集中只包含K个字符,可以用K进制数来表示一个子串。这个K进制数转化成十进制数,作为子串的哈希值。举个例子,比如要处理的字符串只包含a-z这26个小写字母,那就用二十六进制来表示一个字符串。把a-z映射到0-25这26个数字,a就表示0,b就表示1.以此类推,z表示25。
“c b a“ = ‘ c ’ ∗ 26 ∗ 26 + ′ b ′ ∗ 26 + ′ a ′ ∗ 1 ‘c’ * 26 * 26 + 'b' * 26 + 'a' * 1 ‘c’∗26∗26+′b′∗26+′a′∗1= 2 ∗ 26 ∗ 26 + 1 ∗ 26 + 0 ∗ 1 2*26*26 + 1*26 + 0*1 2∗26∗26+1∗26+0∗1 = 1353
哈希表相邻子串存在以下规律:
相邻子串s[i-1]和s[i](i表示子串在主串中的起始位置,子串的长度都为m)。使用s[i-1]的哈希值很快的计算出s[i]的哈希值。
我们事先计算好 2 6 0 26^0 260, 2 6 1 26^1 261, 2 6 2 26^2 262… 2 6 m − 1 26^{m-1} 26m−1,并且存储在一个长度为m的数组中,公式中的“次方”就对应数组的下标。当我们需要计算26的x次方时,可以从数组下标为x的位置取值。
分析一下,RK算法的时间复杂度。整个RK算法包含两部分,计算子串哈希值和模式串哈希值与子串哈希值之间的比较。可以通过设计特殊的哈希算法,只需要扫描一遍主串就能计算出所有子串的哈希值,故这部分时间复杂度为O(n)。模式串哈希值与每个子串哈希值之间的比较的时间复杂度为O(1)。总共需要比较n-m+1个子串的哈希值,这部分的时间复杂度也是O(n)。RK算法整体的时间复杂度为O(n)。
除此之外,还存在一个问题——模式串很长,相应的主串中的子串也会很长,通过哈希算法得到的哈希值就可能很大。如果超过计算机整型数据可以表示的范围,该如何解决?
这时候有很多方法,我们可以使用a-z映射为1-26。将字符串对应的字母数字相加,最后得到的和作为哈希值。但这样做哈希冲突概率比较高。可以进一步优化,比如将每个字母从小到大对应素数,而不是1,2,3…这样的自然数,这样冲突的概率就会降低。发生冲突时,有时子串和模式串的哈希值是相同的,但两者并不匹配。解决方法很简单,只需在比对一下子串和模式串本身。如果存在大量的冲突,每次都要再比对子串和模式串本身,那时间复杂度会退化为O( n ∗ m n*m n∗m)。但一般情况下,冲突不会很多,故RK算法的效率还是比BF算法高。
def calhash(a,b):
n,m = len(a),len(b)
hash_val,t = 0,0
for i in range(m-1,-1,-1):
hash_val += (ord(a[t])-96)*(26**(i))
t += 1
s = [hash_val]
for j in range(1,n-m+1):
hash_val = (hash_val - (ord(a[j-1])-96)*(26**(m-1)))*26 + (ord(a[j+m-1])-96)
s.append(hash_val)
return s
def RK(a,b):
l = []
l = calhash(a,b)
pat_hash,t = 0,0
for i in range(len(b)-1,-1,-1):
pat_hash += (ord(b[t])-96)*(26**(i))
t += 1
if pat_hash in l:
return l.index(pat_hash)
else:
return -1
参考资料:王争《数据结构与算法之美》