(笔记整合)字符串匹配基础

本文介绍了三种字符串匹配算法。BF算法即暴力匹配算法,简单但性能不高,实际开发中常用;RK算法通过哈希算法提高比较效率,整体时间复杂度为O(n);BM算法包含坏字符规则和好后缀规则,能跳过一些不匹配情况,最好情况时间复杂度为O(n/m),还可根据内存需求优化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、BF算法

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

在字符串A中查找字符串B,那字符串A就是主串,字符串B就是模式串。把主串的长度记作n,模式串的长度记作m。因为是在主串中查找模式串,所以n>m。

作为最简单、最暴力的字符串匹配算法,BF算法的思想可以用一句话来概括,那就是,我们在主串中,检查起始位置分别是0、1、2…n-m且长度为m的n-m+1个子串,看有没有跟模式串匹配的。
在这里插入图片描述
BF算法的时间复杂度很高,是O(n*m),但在实际的开发中,它却是一个比较常用的字符串匹配算法。为什么这么说呢?

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

二、RK算法

RK算法的全称叫Rabin-Karp算法,是由它的两位发明者Rabin和Karp的名字来命名的。

RK算法的思路是这样的:通过哈希算法对主串中的n-m+1个子串分别求哈希值,然后逐个与模式串的哈希值比较大小。如果某个子串的哈希值与模式串相等,那就说明对应的子串和模式串匹配了(这里先不考虑哈希冲突的问题)。因为哈希值是一个数字,数字之间比较是否相等是非常快速的,所以模式串和子串比较的效率就提高了。
在这里插入图片描述
通过哈希算法计算子串的哈希值的时候,我们需要遍历子串中的每个字符。尽管模式串与子串比较的效率提高了,但是,算法整体的效率并没有提高。有没有方法可以提高哈希算法计算子串哈希值的效率呢?
这就需要哈希算法设计的非常有技巧了。假设要匹配的字符串的字符集中只包含K个字符,可以用一个K进制数来表示一个子串,这个K进制数转化成十进制数,作为子串的哈希值。

比如要处理的字符串只包含a~z这26个小写字母,那就用二十六进制来表示一个字符串。把a~z这26个字符映射到0~25这26个数字,a就表示0,b就表示1,以此类推,z表示25。
在这里插入图片描述
这种哈希算法有一个特点,在主串中,相邻两个子串的哈希值的计算公式有一定关系。
在这里插入图片描述
这里有一个小细节需要注意,那就是26(m-1)这部分的计算,我们可以通过查表的方法来提高效率。我们事先计算好260、261、262……26(m-1),并且存储在一个长度为m的数组中,公式中的“次方”就对应数组的下标。当我们需要计算26的x次方的时候,就可以从数组的下标为x的位置取值,直接使用,省去了计算的时间。

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

三、BM算法

1.BM算法的核心思想
把模式串和主串的匹配过程,看作模式串在主串中不停地往后滑动。当遇到不匹配的字符时,BF算法和RK算法的做法是,模式串往后滑动一位,然后从模式串的第一个字符开始重新匹配。BM算法则是当模式串和主串某个字符不匹配的时候,能够跳过一
些肯定不会匹配的情况,将模式串往后多滑动几位。

2.BM算法原理分析
BM算法包含两部分,分别是坏字符规则(bad character rule)和好后缀规则(good suffix shift)。

坏字符规则:
BM算法的匹配顺序比较特别,它是按照模式串下标从大到小的顺序,倒着匹配的。
从模式串的末尾往前倒着匹配,当我们发现某个字符没法匹配的时候。我们把这个没有匹配的字符叫作坏字符(主串中的字符)。
拿坏字符c在模式串中查找,发现模式串中并不存在这个字符,也就是说,字符c与模式串中的任何字符都不可能匹配。这个时候,我们可以将模式串直接往后滑动三位,将模式串滑动到c后面的位置,再从模式串的末尾字符开始比较。
在这里插入图片描述
这个时候,模式串中最后一个字符d,还是无法跟主串中的a匹配,这个时候,还能将模式串往后滑动三位吗?答案是不行的。因为这个时候,坏字符a在模式串中是存在的,模式串中下标是0的位置也是字符a。这种情况下,可以将模式串往后滑动两位,让两个a上下对齐,然后再从模式串的末尾字符开始,重新匹配。
当发生不匹配的时候,把坏字符对应的模式串中的字符下标记作si。如果坏字符在模式串中存在,我们把这个坏字符在模式串中的下标记作xi。如果不存在,我们把xi记作-1。那模式串往后移动的位数就等于si-xi。(注意,我这里说的下标,都是字符在模式串的下标)。
在这里插入图片描述

利用坏字符规则,BM算法在最好情况下的时间复杂度非常低,是O(n/m)。比如,主串是aaabaaabaaabaaab,模式串是aaaa。每次比对,模式串都可以直接后移四位,所以,匹配具有类似特点的模式串和主串的时候,BM算法非常高效。

不过,单纯使用坏字符规则还是不够的。因为根据si-xi计算出来的移动位数,有可能是负数,比如主串是aaaaaaaaaaaaaaaa,模式串是baaa。不但不会向后滑动模式串,还有可能倒退。所以,BM算法还需要用到“好后缀规则”。

好后缀规则:
好后缀规则实际上跟坏字符规则的思路很类似。当模式串滑动到图中的位置的时候,模式串和主串有2个字符是匹配的,倒数第3个字符发生了不匹配的情况。
在这里插入图片描述
这个时候该如何滑动模式串呢?当然,还可以利用坏字符规则来计算模式串的滑动位数,不过,也可以使用好后缀处理规则。好后缀规则是怎么工作的?
把已经匹配的bc叫作好后缀,记作{u}。拿它在模式串中查找,如果找到了另一个跟{u}相匹配的子串{u*},那我们就将模式串滑动到子串{u*}与主串中{u}对齐的位置。
在这里插入图片描述
当模式串中不存在等于{u}的子串时,直接将模式串滑动到主串{u}的后面。这样做是否有点太过头呢?看下面这个例子。这里面bc是好后缀,尽管在模式串中没有另外一个相匹配的子串{u*},但是如果将模式串移动到好后缀的后面,如图所示,那就会错过模式串和主串可以匹配的情况。
在这里插入图片描述
所以,针对这种情况,不仅要看好后缀在模式串中,是否有另一个匹配的子串,还要考察好后缀的后缀子串,是否存在跟模式串的前缀子串匹配的。
所谓某个字符串s的后缀子串,就是最后一个字符跟s对齐的子串,比如abc的后缀子串就包括c, bc。所谓前缀子串,就是起始字符跟s对齐的子串,比如abc的前缀子串有a,ab。从好后缀的后缀子串中,找一个最长的并且能跟模式串的前缀子串匹配的,假设是{v},然后将模式串滑动到如图所示的位置。
在这里插入图片描述

可以分别计算好后缀和坏字符往后滑动的位数,然后取两个数中最大的,作为模式串往后滑动的位数。这种处理方法还可以避免前面提到的,根据坏字符规则,计算得到的往后滑动的位数,有可能是负数的情况。

BM算法的性能分析及优化:
先来分析BM算法的内存消耗。整个算法用到了额外的3个数组,其中bc数组的大小跟字符集大小有关,suffix数组和prefix数组的大小跟模式串长度m有关。

如果处理字符集很大的字符串匹配问题,bc数组对内存的消耗就会比较多。因为好后缀和坏字符规则是独立的,如果运行的环境对内存要求苛刻,可以只使用好后缀规则,不使用坏字符规则,这样就可以避免bc数组过多的内存消耗。不过,单纯使用好后缀规则的BM算法效率就会下降一些了。

总结:
1、要有优化意识,前面的 BF,RK 算法已经能够满足我们需求了,为什么发明 BM 算法?是为了减少时间复杂度,但是带来的弊端是,优化代码变得复杂,维护成本变高。
2、需要查找,需要减少时间复杂度,应该想到什么?散列表。
3、如果某个表达式计算开销比较大,又需要频繁的使用怎么办?预处理,并缓存。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值