文章内容、图片均来自极客时间。
如何借助哈希算法实现高效字符串匹配
1 概念和用途
字符串匹配:查找一个字符串A在字符串B中是否出现,这个过程就是字符串匹配。A称为模式串,B称为主串。主串的长度记为n,模式串长度记为m。n>mn>mn>m。
在java中有String的indexOf用就是字符串匹配。
字符串匹配有基础的BF算法、高效的KMP算法。
2 BF算法
BF=Brute Force 暴力搜索
算法思想:在主串中,分别从起始位置0,1,2…n-m且长度为m的n-m+1个子串是否和模式串相同。
从算法过程分析,最坏时间复杂度是O(nm)。例如主串是“aaaa…aaa”(省略符号表示有若干个a)。模式串是"aaaab"。因为有n-m+1个子串,每次比较m次。比较总次数是(n−m+1)∗m=n∗m−m2+m(n-m+1)*m=n*m-m^2+m(n−m+1)∗m=n∗m−m2+m,记为O(nm)。
BF尽管时间复杂度比较高,但在实际应用中经常使用。原因有二。
第一,在实际应用中字符串都比较短,每次两个字符串比较的时候遇到不相同的字符,就可以提前返回。所以实际比n*m要少。
第二,算法思想简单,实现简单,不容易出错。这也符合KISS(Keep it simple and stupid)原则。在工程中,满足性能的前提下,简单是原则。
3 RK算法
RK=Rabin-Karp,是该算法的两位提出者。
在BF算法中,两次字符串比较,因为要比较m次,复杂度较高。如果为每个字符串计算一个哈希值,每次比较哈希值,复杂度就可能会降低。
算法思想:对模式串求哈希值。对n-m+1个主串中的子串分别求哈希值,然后逐个和模式串的哈希值比较。因为哈希值是一个数字比较非常快。但是因为求哈希所以整体算法效率没有提高。
改进:可以通过改进求子串哈希值,降低时间复杂度。假如字符串中只包含a-z这26个字母,每个字母分别对应数字0~25。哈希值采用26进制表示。
例如:"dbc"=d∗26∗26+b∗26+c=3∗26∗26+1∗26+2=2056"dbc" = d*26*26+b*26+c=3*26*26+1*26+2=2056"dbc"=d∗26∗26+b∗26+c=3∗26∗26+1∗26+2=2056
这种哈希方法相邻子串哈希值是有特点的。可以使用前一个子串的哈希值计算下一个子串的哈希值。
记h1=hash(dbc)=32626+126+2,h2=hash(bce)=12626+226+4.
A=126+2
B=12626+226
那么B=26*A
令h[i-1]表示(i-1,i+m-2)子串的哈希值,h[i]表示(i,i+m-1)子串的哈希值。那么
h[i]=(h[i−1]−26m−1(s[i]−′a′))∗26+(s[i+m−1]−′a′)h[i]=(h[i-1]-26^{m-1}(s[i]-'a'))*26+(s[i+m-1]-'a')h[i]=(h[i−1]−26m−1(s[i]−′a′))∗26+(s[i+m−1]−′a′)
这里还可以提高的地方时,可以提前把26026^0260,26126^1261,…26m−126^{m-1}26m−1存入数组中。次方就是数组下标,便于查询。
时间复杂度分析
计算哈希值主要扫描一遍主串即可,时间复杂度O(n)。计算子串与模式串哈希值比较是否相同,时间复杂度O(1),共需要n-m+1次比较, 时间复杂度O(n)。总体时间复杂度时间复杂度O(n)。
字符串很长,哈希值超出能表示的范围怎么办?
上面的设计完全没有哈希冲突,当允许有哈希冲突,会不会解决呢?上面的算法是进位相乘,如果改为加法,数值就小很多了。例如"dbc"=3+1+2=6"dbc"=3+1+2=6"dbc"=3+1+2=6。这样的话取值范围小多了,但哈希冲突概率会很高。如果每个字母不对应整数,而是对应从小到大不同的素数,这样哈希冲突的概率就会降低了。
如果有哈希冲突了,怎么办?
当两个字符串哈希值相同的时候,就挨个字符串比较,确保字符串真的相同。只是如果这样,当冲突概率很高的时候,RF比BF的效率还低。因为还要计算哈希。
4 思考题
上面讲的是一维字符串的比较,如果是主串和模式串都是一个二维矩阵,怎么匹配呢?
解决思路:可以将二维矩阵转为一个数组,变成一维的,再比较。