字符串匹配算法之BF暴力匹配算法与RK哈希匹配算法

下面几篇数据结构理论方面的文章,将会从字符串匹配算法开始说,因为刷leetcode被很多字符串的题难到了,就来学习极客时间的文章了。

我们使用的字符串查找函数,比如java中的indexOf(),python中的find()函数等,他们底层就是依赖接下来要讲的字符串匹配算法。

字符串匹配算法有很多,会分成四节来说。今天会讲两种比较简单的,好理解的,分别是:BF算法和RK算法。还有后面会介绍到的比较难,但是会更加高效的,他们是:BM算法和KMP算法。

但是上面提到的算法都是单模式串匹配的算法,也就是在一个串中同时查找多个串,他们分别是Trie树和AC自动机。

今天说的算法中:RK算法是BF算法的改进,它巧妙借助了我们前面讲过的哈希算法,让匹配的效率有了很大的提升。

那RK算法是如何借助哈希算法来实现高效字符串匹配的呢

BF算法

BF算法中的Brute Froce的缩写,中文叫做暴力匹配算法,也叫朴素匹配算法。

从名字可以看出,这种算法的字符串匹配方法很暴力,当然也很简单好理解,就是性能不高。

在开始讲解这个算法之前,我先对两个概念下定义。他们分别是 “主串”“模式串” 。我们把主串的长度记做n,模式串的长度记做m。因为我们是在主串中查找模式串,所以n>m。

作为最简单,最暴力的字符串匹配算法,BF算法的思想可以用一句话来概括,那就是,我们在主串中,检查起始位置分别是0、1、2···n-m且长度为m的n-m+1个子串,看有没有跟模式串匹配的。

举一个例子来看:

在这里插入图片描述

从上面的算法思想和例子,我们可以看出,在极端情况下,比如主串是“aaaaaaaaa····aaaaaaa”这种的,模式串为“aaaaaaab”。我们每次都比对m个字符,要比对n-m+1次,所以,这种算法的最坏时间复杂度是O(n*m)。

尽管理论上,BF算法的时间复杂度很高,是O(n*m),但是在实际开发中,缺是一个比较常用的字符串的匹配算法,为什么呢?

第一,在实际的软件开发中,大部分情况下,模式串和主串的长度都不会太长。而且每次模式串与主串中的子串匹配的时候,当中途遇到不能匹配的字符的时候,就可以停止了,不需要把m个字符都比对一下。所以,尽管理论上的最坏的情况时间复杂度是O(n*m),但是统计意义上,算法执行效率要比这个好很多。

第二,朴素字符串的匹配算法思想很简单,代码实现也非常简单。简单意味着不容易出错,如果有bug也容易暴露和修复。在工程中,在满足性能要求的前提下,简单是首选。这也是我们常说的KISS原则。

所以,在实际的开发中,暴力法就足够了。

RK算法

RK算法全称叫做Rabin-Karp算法,我个人觉得,它其实就是刚刚讲的BF算法的升级版。

我们知道关于BF算法,如果模式串长度为m,主串长为n,那在主串中,就会有n-m+1个长度为m的子串,我们只需要暴力地对比这n-m+1个子串和模式串,就可以找出主串与模式串匹配的子串。

但是,每次检查主串与子串是否匹配,需要依次比对每个字符,所以BF算法的时间复杂度就比较高,是O(n*m),和BF算法是一样的。但如果引入哈希,提升占用内存,降低时间复杂度,也是可以的,然后我来介绍一下:

RK算法的思路如下:我们通过哈希算法对n-m+1个子串分别求哈希值,然后逐个与模式串的哈希值比大小。如果某个子串的哈希值与模式串相等,那就说明对应的子串匹配到了。

在这里插入图片描述

不过,通过哈希算法计算子串的哈希值的时候,我们需要遍历子串的每个字符。尽管模式串与子串比较的效率提高了,但是算法整体的效率也并没有提高。有没有方法可以提高哈希算法计算子串哈希值的效率呢?

这就需要哈希算法设计得十分有技巧了:假设我们要匹配的字符串的字符集只包含k个字符,我们可以用一个k进制数来表示一个子串,这个k进制数转化成十进制数,作为子串的哈希值。表述起来有点抽象,我举一个例子:

比如要处理的字符串只包含a~z这26个小写字母,那我们就用二十六进制来表示一个字符串。我们把a~z这 26个字符映射到0~25这26个数字,a就表示0,b就表示1,以此类推,z表示25。

在十进制的表示法中,一个数字的值是通过下面的方式计算出来的。对应到二十六进制,一个包含a到z这 26个字符的字符串,计算哈希的时候,我们只需要把进位从10改成26就可以。

在这里插入图片描述
现在,为了方便解释,在下面的讲解中,我假设字符串中只包含a~z这26 个小写字符,我们用二十六进制来表示一个字符串,对应的哈希值就是二十六进制数转化成十进制的结果。

这种哈希算法有一个特点,在主串中,相邻两个子串的哈希值的计算公式有一定关系。我这有个个例子,你 先找一下规律,再来看我后面的讲解。

在这里插入图片描述

从这里的例子来看,我们很容易就能看出这样的规律:相邻两个子串s[i-1]和s[i],对应的哈希值计算有交集的存在,也就是说,我们可以使用s[i-1]的哈希值很快的计算出s[i]的哈希。如果用公式就是这样子:

在这里插入图片描述

不过有一个小细节需要注意:那就是26^(m-1)这部分的计算,我们通过查表的方法去提高效率。我们事先计算好26 ^ 0、26 ^1、26 ^2……26 ^(m-1),并且存储在一个长度为m的数组内,公式中的次方对应数组的下标,当我们需要计算26的x次方的时候,就可以从数组的下标为x的位置取值就行了。

在这里插入图片描述

然后我们来分析一下,现在的时间复杂度:

整个RK算法分为:

  1. 扫描主串,获取子串的哈希,这部分就是简单的遍历计算,所以是On
  2. 模式串哈希值和每个子串的哈希值比较是O1,总共比较n-m+1个子串,所以时间复杂度还是On,所以复杂度就是On

这里还有一个问题就是,模式串很长,响应的主串中的子串也会很长,通过上面的哈希算法计算得到的哈希值就可能会很大,如果超过了计算机中整形数据能表示的范围,那该如何解决呢?

哈希的算法设计有很多种方式,举刚才情况的例子来说。假设字符串中只包含了a~z这26个字母,那么我们给每一个字符replace成数字,a是1,b是2…z是26,这样哈希值就小很多了。但哈希碰撞的概率就上来了,不过我们也可以手动混淆,降低冲突的概率。

但是新的问题又来了

我们假设,总模式串的哈希值为63,这个63=62+1=60+3…那么就会让不同的模式串有相同的哈希,这又该如何解决呢?

实际上,解决方法很简单,只需要再去对比子串和模式串本身就好了。

所以,哈希算法的冲突概率要相对控制得低一些,如果存在大量冲突,就会导致RK算法的时间复杂度退 化,效率下降。极端情况下,如果存在大量的冲突,每次都要再对比子串和模式串本身,那时间复杂度就会 退化成O(n*m)。但也不要太悲观,一般情况下,冲突不会很多,RK算法的效率还是比BF算法高的。

小结

BF算法是最简单、粗暴的字符串匹配算法,它的实现思路是,拿模式串与主串中是所有子串匹配,看是否 有能匹配的子串。所以,时间复杂度也比较高,是O(n*m),n、m表示主串和模式串的长度。不过,在实际 的软件开发中,因为这种算法实现简单,对于处理小规模的字符串匹配很好用。

RK算法是借助哈希算法对BF算法进行改造,即对每个子串分别求哈希值,然后拿子串的哈希值与模式串的 哈希值比较,减少了比较的时间。所以,理想情况下,RK算法的时间复杂度是O(n),跟BF算法相比,效率 提高了很多。不过这样的效率取决于哈希算法的设计方法,如果存在冲突的情况下,时间复杂度可能会退 化。极端情况下,哈希算法大量冲突,时间复杂度就退化为O(n*m)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值