算法:用于字符串匹配的BF 算法和RK算法
发布日期:2022-03-16 03:25:36 浏览次数:39 分类:技术文章

本文共 3501 字,大约阅读时间需要 11 分钟。

字符串匹配算法可以分为两种:

  • 单模式串匹配的算法,也就是一个串跟一个串进行匹配,主要有BF算法和RK算法。RK 算法是 BF 算法的改进。
  • 多模式串匹配算法,也就是在一个串中同时查找多个串,主要有Trie树和AC自动机

我们必须先了解两个概念:主串和模式串。比方说,我们在字符串 A 中查找字符串 B,那字符串 A 就是主串,字符串 B 就是模式串。我们把主串的长度记作 n,模式串的长度记作 m。因为我们是在主串中查找模式串,所以n>m。

BF 算法

BF 算法中的 BF 是 Brute Force 的缩写,中文叫作暴力匹配算法,也叫朴素匹配算法。从名字可以看出,这种算法的字符串匹配方式很“暴力”,当然也就会比较简单、好懂,但相应的性能也不高。

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

在这里插入图片描述

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

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

  • 第一点,实际的软件开发中,大部分情况下,模式串和主串的长度都不会太长。而且没猜错模式串与主串中的子串匹配的时候,当中途遇到不能匹配的字符的时候,就可以停止了,不需要把m个字符都比对一下。所以,尽管理论上的最坏情况时间复杂度是O(n*m),但是大部分情况下,算法的执行效率都要比这个高很多
  • 第二,朴素字符串匹配算法思想简单,代码实现也非常简单。简单意味着不容易出错,如果有bug也很容易暴露和修复。在工厂中,满足性能要求的前提下,简单是首选。这也是我们常说的KISS(Keep it Simple and Stupid)设计原则。

所以,在实际的软件开发中,绝大部分情况下,朴素的字符串匹配算法就够用了。

请添加图片描述

RK算法

RK 算法的全称叫 Rabin-Karp 算法,是由它的两位发明者 Rabin 和 Karp 的名字来命名的。它其实就是刚刚讲的 BF 算法的升级版。

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

但是,每次检测主串与子串是否匹配,需要依次对比每个字符,所以BF算法的时间复杂度就比较高,是O(n*m)。我们对朴素字符串匹配算法改造一下,引入哈希算法,时间复杂度立即就会降低。

RK算法的思路是这样的:我们通过哈希算法对主串中的n-m+1个子串分别求哈希值,然后逐个与模式串的哈希值比较大小。如果某个子串的哈希值与模式串相等,那就说明对应的子串与模式串匹配了(这里先不考虑哈希冲突的问题)。因为哈希值是一个数字,数字之间比较是否相等是非常快速的,所以模式串和子串比较的效率就提高了。

在这里插入图片描述

请添加图片描述

请添加图片描述

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

这就需要哈希算法设计的非常有技巧了。我们假设要匹配的字符串的字符集中只包含K个字符,我们可以用一个K进制数来表示一个子串,这个K进制数转换成十进制数,作为子串的哈希值。 举个例子:

  • 比如要处理的字符串只包含az这26个小写字母,那我们就用二十六进制来表示一个字符串。我们把az这26个字符映射到0~26这26个数字,a就表示0,b就表示1,以此类推,z表示25
  • 在十进制的表示法中,一个数字的值是通过下面的方式计算出来的。对应到二十六进制,一个包含 a 到 z 这 26 个字符的字符串,计算哈希的时候,我们只需要把进位从 10 改成 26就可以。

在这里插入图片描述

那么RK算法又是怎么应用这个哈希算法的呢?为了方便解释,这里假设字符串只包含a~z这26个小写字符,我们用二十六进制来表说一个字符串,对应的哈希值就是二十六进制数转换成十进制的结果。

这种哈希算法有一个特性,在主串中,相邻两个子串的哈希值的计算公式有一定关系。举个例子

在这里插入图片描述
从上面例子可以看出这样的规律:相邻两个子串s[i-1]和s[i](i表示子串在主串中的起始位置,子串的长度都为m),对应的哈希值计算公式有交集,也就是说,我们可以使用s[i-1]的哈希值很快的计算出s[i]的哈希值。如果用公式表示的话,就是下面这样的:
在这里插入图片描述
不过,这里有一个小细节需要注意,那就是 2 6 m − 1 26^{m-1} 26m1这部分的计算,我们可以通过查表法来提高效率。我们事先计算好 2 6 0 、 2 6 1 . . . 2 6 m − 1 26^0、26^1...26^{m-1} 260261...26m1,并且存储在一个长度为m的数组中,公式中的次方就对应数组的下标。当我们需要计算26的x次方的时候,就可以从数组的下标为x的位置取值,直接使用,省去了计算的时间。

在这里插入图片描述

那RK 算法的时间复杂度到底是多少呢?

  • 整个RK算法包含两部分,计算子串哈希值和模式串哈希值与子串哈希值之间的比较。
    • 第一部分,可以通过设计特殊的哈希算法,只需要扫描一遍主串就能计算出所有子串的哈希值,所以这部分的时间复杂度是O(n)
    • 第二部分,模式串哈希值与每个子串哈希值之间比较的时间复杂度是O(1),总共需要比较n-m+1个子串的哈希值,所以,这部分的时间复杂度也是O(n)
  • 所以,整个RK算法的时间复杂度是O(n)

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

  • 刚刚我们设计的哈希顺丰是没有散列冲突的,也就是说,一个字符串与一个二十六进制数一一对应,不同的字符串的哈希值肯定不一样。因为我们是基于进制来表示一个字符串的。实际上,我们为了能将哈希值落在整形数据范围内,可以牺牲一下,允许哈希冲突。这个时候哈希算法该如何设计呢?

  • 哈希算法的设计方法有很多,举个例子:。假设字符串中只包含 a~z 这 26 个英文字母,那我们每个字母对应一个数字,比如 a 对应 1,b 对应 2,以此类推,z 对应26。我们可以把字符串中每个字母对应的数字相加,最后得到的和作为哈希值。这种哈希算法产生的哈希值的数据范围就相对要小很多了。

  • 不过,这种哈希算法的哈希冲突概率也是挺高的。当然,还有很多更加优化的方法,比如将每一个字母从小到大对应一个素数,而不是 1,2,3……这样的自然数,这样冲突的概率就会降低一些。

那现在新的问题来了之前我们只需要比较一下模式串和子串的哈希值,如果两个值相等,那这个子串就一定可以匹配字符模式串。但是,当存在哈希冲突时,有可能存在这样的情况,子串和模式串的哈希值虽然是相同的,但是两者本身并不匹配。

实际上,解决方法很简单。当我们发现一个子串的哈希值跟模式串的哈希值相等的时候,我们只需要再对比一下子串和模式串本身就好了。当然,如果子串的哈希值和模式串的哈希值不相等,那对应的子串和模式串肯定也是不匹配的,就不需要比对子串和模式串本身了。

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

小结

  • BF算法是最简单、粗暴的字符串匹配算法,它的实现思路是,拿模式串与主串中所有子串匹配,看是否能有匹配的子串。所以,时间复杂度也比较高,是O(n*m),n、m表示主串和模式串的长度。不过,在实际的软件开发中,因为这种算法实现简单,对于处理小规模的字符串匹配很好用
  • RK算法是借助哈希算法对BF算法进行改造,即对每个子串分别求哈希值,然后那子串的哈希值和模式串的哈希值比较,减少了比较的时间。所以,理想情况下,RK算法的时间复杂度是O(n),跟BF算法相比,效率提高了很多。不过这样的效率取决于哈希算法的设计方法,如果存在冲突的情况下,时间复杂度可能会退化。极端情况下,哈希算法大量冲突,时间复杂度就退化为O(n*m)

转载地址:https://blog.csdn.net/zhizhengguan/article/details/122660876 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:C/C++编程:STL关联式容器源码学习
下一篇:C/C++面试:容器的元素可以声明引用类型吗?

发表评论

最新留言

路过按个爪印,很不错,赞一个!
[***.219.124.196]2024年04月24日 12时26分11秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章