数据结构与算法之美-字符串匹配(上)

BF (Brute Force) 暴力/朴素匹配算法

主串和模式串

我们在字符串 A 中查找字符串 B,那字符串 A 就是主串,字符串 B 就是模式串。

我们把主串的长度记作 n,模式串的长度记作 m。因为我们是在主串中查找模式串,所以 n>m。

BF算法思想

在主串中,检查起始位置分别是 0、1、2…n-m 且长度为 m 的 n-m+1 个子串,看有没有跟模式串匹配的。

BF算法的缺点

在极端情况下,如主串是“aaaaa…aaaaaa”,模式串是“aaaaab”。我们每次都比对 m 个字符,需要比对 n-m+1 次。这种算法的最坏情况时间复杂度是 O(n*m)。

但在实际开发在BF是一种常用的字符串匹配算法。因为实际的软件开发中,大部分情况下,模式串和主串的长度都不会太长,算法执行效率要比 O(n*m)高很多。而且朴素字符串匹配算法思想简单,代码实现也非常简单。

RK (Rabin-Karp) 算法

它其实就是 BF 算法的升级版。对朴素的字符串匹配算法稍加改造,引入哈希算法,时间复杂度立刻就会降低。

RK算法思想

通过哈希算法对主串中的 n-m+1 个子串分别求哈希值,然后逐个与模式串的哈希值比较大小。

如果某个子串的哈希值与模式串相等,那就说明对应的子串和模式串匹配了,这里先不考虑哈希冲突的问题。

哈希值是一个数字,数字之间比较是否相等是非常快速的,所以模式串和子串比较的效率就提高了。

巧妙的哈希算法

通过哈希算法计算子串的哈希值的时候,需要遍历子串中的每个字符。尽管模式串与子串比较的效率提高了,但是算法整体的效率并没有提高。这就需要设计一个非常巧妙的哈希算法了。

假设要匹配的字符串的字符集中只包含 K 个字符,我们可以用一个 K 进制数来表示一个子串,这个 K 进制数转化成十进制数,作为子串的哈希值。

假设要处理的字符串只包含 a~z 这 26 个小写字母,那就用二十六进制来表示一个字符串。计算哈希值的时候,我们只需要把进位从 10 改成 26 就可以。

这种哈希算法有一个特点,在主串中,相邻两个子串的哈希值的计算公式有一定关系。即可以使用 s[i-1] 的哈希值很快的计算出 s[i] 的哈希值。公式如下所示:

//其中, h[i]、h[i-1] 分别对应 s[i] 和 s[i-1] 两个子串的哈希值
h[i] = 26*(h[i-1]-26^(m-1)*(s[i-1]-‘a‘)) + (s[i+m-1]-‘a‘);

其中 26^(m-1) 这部分的计算,可以通过查表的方法来提高效率。事先计算好 26^0、26^1等等,并且存储在一个长度为 m 的数组中。

公式中的“次方”就对应数组的下标。需要计算 26 的 x 次方的时候,就可以从数组的下标为 x 的位置取值,省去了计算的时间。

综上所述,可得RK 算法整体的时间复杂度是 O(n)。

哈希算法的缺点

模式串很长,相应的主串中的子串也会很长,通过上面的哈希算法计算得到的哈希值就可能很大,可能会超过了计算机中整型数据可以表示的范围。

前面设计的哈希算法是没有散列冲突的。因此,为了能将哈希值落在整型数据范围内,是可以牺牲一下,允许哈希冲突的。比如将26进制转为10进制的算法改为数字相加。

当存在哈希冲突的时候,有可能子串和模式串的哈希值虽然是相同的,但是两者本身并不匹配。我们只需要再对比一下子串和模式串本身就好了。

如果存在大量冲突,就会导致 RK 算法的时间复杂度退化,效率下降。极端情况下,如果存在大量的冲突,每次都要再对比子串和模式串本身,那时间复杂度就会退化成 O(n*m)。

原文地址:https://www.cnblogs.com/errornull/p/10168298.html

时间: 2024-11-10 14:20:06

数据结构与算法之美-字符串匹配(上)的相关文章

《数据结构与算法之美》——冒泡排序、插入排序、选择排序

排序,是每一本数据结构的书都绕不开的重要部分. 排序的算法也是琳琅满目.五花八门. 每一个算法的背后都是智慧的结晶,思想精华的沉淀. 个人觉得排序算法没有绝对的孰优孰劣,用对了场景,就是最有的排序算法. 当然,撇开这些业务场景,排序算法本身有一些自己的衡量指标,比如我们经常提到的复杂度分析. 我们如何分析一个算法? 排序算法的执行效率 1.最好.最坏和平均情况的时间复杂度 2.时间复杂度的系数.常数和低阶 一般来说,在数据规模n很大的时候,可以忽略这些,但是如果我们需要排序的数据规模在几百.几千

JavaScript 数据结构与算法之美 - 栈内存与堆内存 、浅拷贝与深拷贝

前言 想写好前端,先练好内功. 栈内存与堆内存 .浅拷贝与深拷贝,可以说是前端程序员的内功,要知其然,知其所以然. 笔者写的 JavaScript 数据结构与算法之美 系列用的语言是 JavaScript ,旨在入门数据结构与算法和方便以后复习. 栈 定义 后进者先出,先进者后出,简称 后进先出(LIFO),这就是典型的栈结构. 新添加的或待删除的元素都保存在栈的末尾,称作栈顶,另一端就叫栈底. 在栈里,新元素都靠近栈顶,旧元素都接近栈底. 从栈的操作特性来看,是一种 操作受限的线性表,只允许在

JavaScript 数据结构与算法之美 - 冒泡排序、插入排序、选择排序

1. 前言 算法为王. 想学好前端,先练好内功,只有内功深厚者,前端之路才会走得更远. 笔者写的 JavaScript 数据结构与算法之美 系列用的语言是 JavaScript ,旨在入门数据结构与算法和方便以后复习. 之所以把冒泡排序.选择排序.插入排序放在一起比较,是因为它们的平均时间复杂度都为 O(n2). 请大家带着问题:为什么插入排序比冒泡排序更受欢迎 ?来阅读下文. 2. 如何分析一个排序算法 复杂度分析是整个算法学习的精髓. 时间复杂度: 一个算法执行所耗费的时间. 空间复杂度:

JavaScript 数据结构与算法之美 - 归并排序、快速排序、希尔排序、堆排序

1. 前言 算法为王. 想学好前端,先练好内功,只有内功深厚者,前端之路才会走得更远. 笔者写的 JavaScript 数据结构与算法之美 系列用的语言是 JavaScript ,旨在入门数据结构与算法和方便以后复习. 之所以把归并排序.快速排序.希尔排序.堆排序放在一起比较,是因为它们的平均时间复杂度都为 O(nlogn). 请大家带着问题:快排和归并用的都是分治思想,递推公式和递归代码也非常相似,那它们的区别在哪里呢 ? 来阅读下文. 2. 归并排序(Merge Sort) 思想 排序一个数

算法题之字符串匹配问题

我最近复习一道困难程度的算法题,发现了许多有趣之处.在借鉴了他人解法后,发现从最简单的情况反推到原题是一种解锁新进阶的感觉.从递归到动态规划,思维上一步一步递进,如同一部跌宕起伏的小说,记录下来和诸君共赏之. 题目如下: 给你一个字符串?s?和一个字符规律?p,请你来实现一个支持 '.'?和?'*'?的正则表达式匹配. '.' 匹配任意单个字符 '*' 匹配零个或多个前面的那一个元素 所谓匹配,是要涵盖?整个?字符串?s的,而不是部分字符串. 说明: s?可能为空,且只包含从?a-z?的小写字母

数据结构与算法之美-堆的应用

堆的应用一:优先级队列 优先级队列首先应该是一个队列.队列最大的特性就是先进先出.但是在优先级队列中,出队顺序不是先进先出,而是按照优先级来,优先级最高的,最先出队. 用堆来实现优先级队列是最直接.最高效的.这是因为,堆和优先级队列非常相似.一个堆就可以看作一个优先级队列.很多时候,它们只是概念上的区分而已. 往优先级队列中插入一个元素,就相当于往堆中插入一个元素.从优先级队列中取出优先级最高的元素,就相当于取出堆顶元素. 很多数据结构和算法都要依赖它.比如,赫夫曼编码.图的最短路径.最小生成树

十分钟弄懂:数据结构与算法之美 - 时间和空间复杂度

复杂度分析是整个算法学习的精髓,只要掌握了它,数据结构和算法的内容基本上就掌握了一半了. 1. 什么是复杂度分析 ? 数据结构和算法解决是 "如何让计算机更快时间.更省空间的解决问题". 因此需从执行时间和占用空间两个维度来评估数据结构和算法的性能. 分别用时间复杂度和空间复杂度两个概念来描述性能问题,二者统称为复杂度. 复杂度描述的是算法执行时间(或占用空间)与数据规模的增长关系. 2. 为什么要进行复杂度分析 ? 和性能测试相比,复杂度分析有不依赖执行环境.成本低.效率高.易操作.

《数据结构与算法之美》 <03>数组:为什么很多编程语言中数组都从0开始编号?

提到数组,我想你肯定不陌生,甚至还会自信地说,它很简单啊. 是的,在每一种编程语言中,基本都会有数组这种数据类型.不过,它不仅仅是一种编程语言中的数据类型,还是一种最基础的数据结构.尽管数组看起来非常基础.简单,但是我估计很多人都并没有理解这个基础数据结构的精髓. 在大部分编程语言中,数组都是从 0 开始编号的,但你是否下意识地想过,为什么数组要从 0 开始编号,而不是从 1 开始呢? 从 1 开始不是更符合人类的思维习惯吗? 你可以带着这个问题来学习接下来的内容. 如何实现随机访问? 什么是数

数据结构与算法之美-堆和堆排序

堆和堆排序 如何理解堆 堆是一种特殊的树,只要满足以下两点,这个树就是一个堆. ①完全二叉树,完全二叉树要求除了最后一层,其他层的节点个数都是满的,最后一层的节点都靠左排列. ②树中每一个结点的值都必须大于等于(或小于等于)其子树中每个节点的值.大于等于的情况称为大顶堆,小于等于的情况称为小顶堆. 如何实现堆 如何存储一个堆 完全二叉树适合用数组来存储,因为数组中对于下标从1开始的情况,下标为i的节点的左子节点就是下标为i*2的节点,右子节点就是i下标为i*2+1的节点,其父节点时下标为i/2的