当前位置: 首页 > news >正文

数据结构:串 及串的模式匹配(KMP)

串的定义

串(String)是由零个或多个字符组成的有限序列,又名叫字符串。在计算机科学中,串是一种重要的数据结构,用于表示文本数据。串中的元素称为字符,字符可以是字母、数字或其他符号,这些字符可以是任意字符集中的成员。串是许多编程语言中的基本数据类型,用于处理文本数据。

串的基本操作

无论是基于数组还是基于链表的实现,串都支持以下基本操作:

  • StrAssign(&S, chars):赋值操作,将串chars的值赋给串S。
  • StrLength(S):求串的长度,返回串S的元素个数。
  • StrCopy(&T, S):串复制,将串S复制给串T。
  • StrConcat(&T, S1, S2):串连接,将串S1和串S2连接成新串T。
  • StrCompare(S1, S2):串比较,比较串S1和串S2,若S1=S2,则返回0;若S1<S2,则返回负数;若S1>S2,则返回正数。
  • SubString(&Sub, S, pos, len):子串获取,用Sub返回串S中第pos个字符起长度为len的子串。
  • StrInsert(&S, pos, T):串插入,将串T插入到串S的第pos个位置。
  • StrDelete(&S, pos, len):串删除,从串S中删除第pos个字符起长度为len的子串。

串的实现

串的实现方式主要有两种:基于数组(或称为顺序存储结构)和基于链表(或称为链式存储结构)。

1. 基于数组的实现

在基于数组的实现中,串的存储结构是用一个字符数组以及记录串的实际长度的变量来表示。这种实现方式可以方便地通过下标访问串中的任意字符,时间复杂度为O(1)。但是,当需要插入或删除串中的字符时,可能需要移动大量的字符,时间复杂度较高。

#define MAXLEN 255
typedef struct
{char ch[MAXLEN];int length;
}SString;//动态分配 克服字符串超过最大长度的"截断"
typedef struct 
{char *ch;int length;
}HString;

2. 基于链表的实现

在基于链表的实现中,串的每个字符都存储在链表的一个节点中。这种实现方式在插入和删除字符时非常高效,因为只需要修改指针,时间复杂度为O(1)。但是,访问串中的任意字符时,可能需要从头节点遍历链表,时间复杂度为O(n)。

串的模式匹配:

int index(SString S,SString T){int i=1;int j=1;while(i<=S.length&&j<=T.length){if(S.ch[i]==T.ch[j]){i++;j++;}else{i=i-j+2;//重新开始匹配j=1;}}if(j>T.length)return i-T.length;elsereturn 0;
}

图解(举例):

KMP算法:

(关于KMp算法中如何求next数组以及 算法中的指针变换在我写的另一篇博客中) 

几个概念:

1、字符串的前缀、后缀和部分匹配值
要了解子串的结构,首先要弄清楚几个概念:前缀、后缀和部分匹配值。前缀指除最后一个字符以外,字符串的所有头部子串;后缀指除第一个字符外,字符串的所有尾部子串;部分匹配值则为字符串的前缀和后缀的最长相等前后缀长度。下面以' ababa '为例进行说明:
' a '的前缀和后缀都为空集,最长相等前后缀长度为0。
' ab '的前缀为{ a },后缀为( b },{ a } ∩ { b }=0,最长相等前后缀长度为0。

' aba '的前缀为{ a  ab ),后缀为( a , ba },( a , ab } ∩ { a , ba }={ a ),最长相等前后缀长度为1。
' abab '的前缀( a , ab , aba } ∩ 后缀( b , ab , bab }={ ab ),最长相等前后缀长度为2。
' ababa '的前缀{ a , ab , aba , abab ) ∩ 后缀( a , ba , aba , baba }={ a , aba ),公共元素有两个,最长相等前后缀长度为3。
因此,字符串' ababa 的部分匹配值(PM表为00123。

右移位数=已匹配的字符数-对应的部分匹配值
Move=(j-1)-PM[j-i]

next数组就是将PM表的值全部向右移1位,即字符串' ababa 的next值为-1 0 0 1 2(这样哪个元素匹配失败就直接找next数组里自己对应的值,不用去找匹配失败元素的前一个元素的PM值。

Move=(j-1)-next[j]
//j回退到
j=j-Move=j-((j-1)-next[j])=next[j]+1

为了使公式更加简洁/计算方便可以将next数组整体+1,即 即字符串' ababa 的next值为0 1 1 2 3

实现过程:

为了避免朴素算法的低效,几位计算机科学家辈D.E.Knuth、J.H.MorTis和V.R.Pratt发表了一个模式匹配算法,可以一定程度上避免重复遍历的问题,该算法就是大名鼎鼎的KMP算法。

从暴力匹配到KMP
要理解KMP算法的原理,我们首先需要批判一下朴素算法中有哪些做的不好的地方。

我们将之前的朴素算法的匹配过程合起来看如下面的图所示:

我们可以发现,在2、3、4步骤中,主串的首字符与子串的首字符均不等。我们仔细观察可以发现,对于子串"google"来说,首字母"g"与后面的两个字母是不相等的,而在步骤1中主串与子串的前三个字符相等,这就意味着子串的首字符"g"不可能与主串的二、三位相等,故上图中步骤2、3完全是多余的。

也就是说,如果我们知道子串的首字符"g"与后面两个字符不相等(此处需要进行一些预处理,这是后面的重点),我们就不需要再进行2、3步操作,只保留1、4、5步即可。


从上面这幅图我们可以惊喜地发现,在使用朴素算法进行匹配时,主串指针需要进行一些回退;而在知道了子串的一些性质后,主串指针不需要再进行回退,只需一直往前走就行,这样就节省了一些时间开销。

你或许还会有疑问,主串指针是不需要回退了,但为啥我的子串指针还要一直回退到开头呢,有没有办法避免这样呢?

当然是有的,我们再举一个例子,假设主串S=“abcababca”,子串T=“abcabx”,那我们采用朴素算法在进行模式匹配的步骤如下所示:


由之前一个例子的分析可知由于T串的首字母"a"与之后的字母"b"、"c"不等,故步骤2、3均是可以省略的。

又因为T的首位"a"与T第四位的"a"相等,第二位的"b"与第五位的"b"相等。而在步骤1中,第四位的"a"与第五位的"b"已经与主串s中的相应位置比较过了是相等的。因此可以断定, T的首字符“a”、第二位的字符“b”与S的第四位字符和第五位字符也不需要比较了,肯定也是相等的。所以步骤4、5这两个比较得出字符相等的步骤也可以省略。

所以在这个例子中我们模式匹配的流程为:


在这个例子中我们发现,在得知了子串中有相等的前后缀之后,匹配失败时子串指针不需要回退到开头处,而是回退到相等前缀的后一个位置。

补充一下前后缀的概念:前缀指的是不包含尾字符的所有子串;后缀指的是不包含首字符的所有子串

对比两个例子中朴素做法与改进做法的差别,我们可以发现这两种方法的相同点为都是依靠两个指针的移动对比来实现字符串模式匹配,不同之处在于在改进做法中主串的指针i不需要进行回溯,子串的指针j会根据子串中的最长相等前后缀来进行回溯,这样就省去了一些不需要的回溯步骤,从而降低了时间复杂度。

注意事项

  • 在使用串时,应注意串的边界条件,如索引越界等问题。
  • 根据不同的应用场景,选择合适的串实现方式,以达到最优的性能。
  • 在某些编程语言中,字符串的实现可能更加复杂,如Python中的字符串是不可变的,这意呀着每次修改字符串都会生成一个新的字符串对象。

相关文章:

  • Cortex-A7和Cortex-M7架构处理器取中断向量全流程分析
  • 单片机串口AT指令操作SIM800、900拨打电话
  • 【QT 开发日志】QT 基础控件详解:按钮、文本框与标签的使用
  • 量化交易backtrader实践(三)_指标与策略篇(1)_指标简介与手工双均线策略
  • C语言课程设计题目六:学生信息管理系统设计
  • OpenCV视频I/O(10)视频采集类VideoCapture之从视频流中检索一帧图像函数 retrieve()的使用
  • Java面试常见问题总结
  • L8打卡学习笔记
  • [数据集][目标检测]猪数据集VOC-2856张
  • 开放式蓝牙耳机哪个品牌更靠谱?5款高性价比开放式耳机推荐
  • RHCS认证-Linux(RHel9)-Ansible
  • 元宇宙的未来趋势:Web3的潜在影响
  • 集成MinIO实现文件存储管理:文件上传、文件下载、文件删除、获取文件访问地址、获取文件访问地址
  • ESP32 Bluedroid 篇(1)—— ibeacon 广播
  • Error和Exception
  • [LeetCode] Wiggle Sort
  • 【Redis学习笔记】2018-06-28 redis命令源码学习1
  • Angular 4.x 动态创建组件
  • javascript面向对象之创建对象
  • Java方法详解
  • REST架构的思考
  • 关于Android中设置闹钟的相对比较完善的解决方案
  • 关于字符编码你应该知道的事情
  • 基于遗传算法的优化问题求解
  • 判断客户端类型,Android,iOS,PC
  • 为物联网而生:高性能时间序列数据库HiTSDB商业化首发!
  • 小程序开发中的那些坑
  • 要让cordova项目适配iphoneX + ios11.4,总共要几步?三步
  • # C++之functional库用法整理
  • #我与Java虚拟机的故事#连载09:面试大厂逃不过的JVM
  • (02)Unity使用在线AI大模型(调用Python)
  • (1)Nginx简介和安装教程
  • (10)STL算法之搜索(二) 二分查找
  • (C#)Windows Shell 外壳编程系列9 - QueryInfo 扩展提示
  • (第30天)二叉树阶段总结
  • (动态规划)5. 最长回文子串 java解决
  • (附源码)ssm本科教学合格评估管理系统 毕业设计 180916
  • (紀錄)[ASP.NET MVC][jQuery]-2 純手工打造屬於自己的 jQuery GridView (含完整程式碼下載)...
  • (深入.Net平台的软件系统分层开发).第一章.上机练习.20170424
  • (一)【Jmeter】JDK及Jmeter的安装部署及简单配置
  • (一)使用IDEA创建Maven项目和Maven使用入门(配图详解)
  • (转)Linq学习笔记
  • (转载)虚幻引擎3--【UnrealScript教程】章节一:20.location和rotation
  • ***汇编语言 实验16 编写包含多个功能子程序的中断例程
  • .form文件_一篇文章学会文件上传
  • .NET 6 Mysql Canal (CDC 增量同步,捕获变更数据) 案例版
  • .NET C#版本和.NET版本以及VS版本的对应关系
  • .NET CF命令行调试器MDbg入门(二) 设备模拟器
  • .NET Core 发展历程和版本迭代
  • .net framework 4.8 开发windows系统服务
  • .NET/C# 在 64 位进程中读取 32 位进程重定向后的注册表
  • .Net下C#针对Excel开发控件汇总(ClosedXML,EPPlus,NPOI)
  • @Transactional注解下,循环取序列的值,但得到的值都相同的问题
  • [2016.7.test1] T2 偷天换日 [codevs 1163 访问艺术馆(类似)]
  • [20171102]视图v$session中process字段含义