数据结构复习指导之串的模式匹配

文章目录

串的模式匹配

考纲内容

复习提示

1.简单的模式匹配算法

知识回顾

2.串的模式匹配算法——KMP算法

2.1字符串的前缀、后缀和部分匹配值

2.2KMP算法的原理是什么

3.KMP算法的进一步优化


串的模式匹配

考纲内容

字符串模式匹配

复习提示

本章是统考大纲第6章内容,采纳读者建议单独作为一章,大纲只要求掌握字符串模式匹配重点掌握 KMP 匹配算法的原理 next数组的推理过程,手工求 next 数组可以先计算出部分匹配值表然后变形,或根据公式来求解。了解nextval数组的求解方法

1.简单的模式匹配算法

子串的定位操作通常称为串的模式匹配,它求的是子串(常称模式串)在主串中的位置。这里采用定长顺序存储结构,给出一种不依赖于其他串操作的暴力匹配算法。

int Index(sString S,sString T){int i=1,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;else return 0;
}

在上述算法中,分别用计数指针 i 和 j 指示主串S和模式串T中当前正待比较的字符位置。

算法思想为:从主串S的第一个字符起,与模式串T的第一个字符比较,若相等,则继续逐个比较后续字符;否则从主串的下一个字符起,重新和模式串的字符比较;以此类推,直至模式串T中的每个字符依次和主串S中的一个连续的字符序列相等,则称匹配成功,函数值为与模式串T中第一个字符相等的字符在主串S中的序号,否则称匹配不成功,函数值为零。图 4.2展示了模式串 T='abcac'和主串S的匹配过程,每次匹配失败后,都把模式串T后移一位。

简单模式匹配算法的最坏时间复杂度为 O(nm),其中n和m分别为主串和模式串的长度

例如,当模式串为'0000001'而主串为'0000000000000000000000000000000000000000 000001'时,由于模式串中的前6个字符均为"0',主串中的前 45 个字符均为'0',每趟匹配都是比较到模式串中的最后一个字符时才发现不等,指针i需要回溯 39 次,总比较次数为 40x7=280 次。

知识回顾

2.串的模式匹配算法——KMP算法

根据图 4.2 的匹配过程,在第三趟匹配中,i=7、j=5 的字符比较,结果不等,于是又从 i=4、j=1 重新开始比较。然而,仔细观察会发现,i=4和 j=1,i=5 和j=1及 i=6和 j=1 这三次比较都是不必进行的。从第三趟部分匹配的结果可知,主串的第4个、第5个和第6个字符是'b'、'c'和'a'(即模式串的第2 个、第3个和第4个字符),因为模式串的第1个字符是'a',所以无须再和这三个字符进行比较,而只需将模式串向右滑动三个字符的位置,继续进行 i=7、j=2 时的比较即可。

在暴力匹配中,每趟匹配失败都是模式串后移一位再从头开始比较。而某趟已匹配相等的字符序列是模式串的某个前缀,这种频繁的重复比较相当于模式串不断地进行自我比较,这就是其低效率的根源。因此,可以从分析模式串本身的结构着手,若已匹配相等的前缀序列中有某个后缀正好是模式串的前缀,则可将模式串向后滑动到与这些相等字符对齐的位置,主串 i 指针无须回溯,并从该位置开始继续比较。而模式串向后滑动位数的计算仅与模式串本身的结构有关,而与主串无关(这里理解起来比较困难,没关系,带着这个问题继续往后看)。

2.1字符串的前缀、后缀和部分匹配值

要了解子串的结构,首先要弄清楚几个概念:前缀、后缀和部分匹配值

前缀指除最后一个字符以外,字符串的所有头部子串;

后缀指除第一个字符外,字符串的所有尾部子串;

部分匹配值则为字符串的前缀和后缀的最长相等前后缀长度。下面以'ababa'为例进行说明:

  • 'a'的前缀和后缀都为空集,最长相等前后缀长度为0。
  • 'ab'的前缀为{a},后缀为{b},{a}n{b}=Ø,最长相等前后缀长度为 0。
  • 'aba'的前缀为{a,ab},后缀为{a,ba},{a,ab}n{a,ba}={a},最长相等前后缀长度为 1。
  • 'abab'的前缀{a,ab,aba}n后缀{b,ab,bab}={ab},最长相等前后缀长度为 2.
  • 'ababa'的前缀{a,ab,aba,abab}∩后缀{a,ba,aba,baba}={a,aba},公共元素有两个,最长相等前后缀长度为3。

因此,字符串'ababa'的部分匹配值为 00123.

这个部分匹配值有什么作用呢?
回到最初的问题,主串为 ababcabcacbab,子串为abcac。
利用上述方法容易写出子串'abcac'的部分匹配值为 00010,将部分匹配值写成数组形式,就得到了部分匹配值(Partial Match,PM)的表。

                        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        

下面用 PM 表来进行字符串匹配:

第一趟匹配过程:
发现 c与a不匹配,前面的2个字符'ab'是匹配的,查表可知,最后一个匹配字符b对应的部分匹配值为0,因此按照下面的公式算出子串需要向后移动的位数:
                                                                                移动位数=已匹配的字符数-对应的部分匹配值
因为2-0=2,所以将子串向后移动2位,如下进行第二趟匹配:



第二趟匹配过程:
发现 c与b不匹配,前面4个字符'abca'是匹配的,最后一个匹配字符a对应的部分匹配值为1,4-1=3,将子串向后移动3位,如下进行第三趟匹配:

第三趟匹配过程:
子串全部比较完成,匹配成功。整个匹配过程中,主串始终没有回退,所以KMP算法可以在 O(n+m)的时间数量级上完成串的模式匹配操作,大大提高了匹配效率。

某趟发生失配时,若对应的部分匹配值为 0,则表示已匹配相等序列中没有相等的前后缀,此时移动的位数最大,直接将子串首字符后移到主串当前位置进行下一趟比较:若已匹配相等序列中存在最大相等前后缀(可理解为首尾重合),则将子串向右滑动到和该相等前后缀对齐(这部分字符下一趟显然不需要比较),然后从主串当前位置进行下一趟比较。

2.2KMP算法的原理是什么

我们刚刚学会了怎样计算字符串的部分匹配值、怎样利用子串的部分匹配值快速地进行字符串匹配操作,但公式“移动位数=已匹配的字符数-对应的部分匹配值”的意义是什么呢?

如图4.3所示,当c与b不匹配时,已匹配'abca'的前缀a和后缀a为最长公共元素。已知前缀a与 b、c均不同,与后缀a相同,因此无须比较,直接将子串移动“已匹配的字符数-对应的部分匹配值”,用子串前缀后面的元素与主串匹配失败的元素开始比较即可,如图 4.4所示。

对算法的改进方法:
已知:右移位数=已匹配的字符数-对应的部分匹配值。
写成:Move=(j-1)-PM[j-1]。
使用部分匹配值时,每当匹配失败,就去找它前一个元素的部分匹配值,这样使用起来有些不方便,所以将 PM 表右移一位,这样哪个元素匹配失败,直接看它自己的部分匹配值即可。

将上例中字符串'abcac'的PM 表右移一位,就得到了next 数组:

我们注意到:
1) 第一个元素右移以后空缺的用-1来填充,因为若是第一个元素匹配失败,则需要将子串向右移动一位,而不需要计算子串移动的位数。
2) 最后一个元素在右移的过程中溢出,因为原来的子串中,最后一个元素的部分匹配值是其下一个元素使用的,但显然已没有下一个元素,所以可以舍去。

这样,上式就改写为
                                                                Move=(j-1)-next[j]
相当于将子串的比较指针 j 回退到
                                                                j=j-Move=j-((j-1)-next[j])=next[j]+1
有时为了使公式更加简洁、计算简单,将next 数组整体+1。
因此,上述子串的 next 数组也可以写成:

[命题追踪——KMP 匹配过程中指针变化的分析]

最终得到子串指针变化公式 j=next[ j ]。在实际匹配过程中,子串在内存中是不会移动的,而是指针发生变化,画图举例只是为了让问题描述得更形象。next[ j ]的含义是:当子串的第 j 个字符与主串发生失配时,跳到子串的 next[ j ]位置重新与主串当前位置进行比较

如何推理 next 数组的一般公式?设主串为's_{1}s_{2}...s_{n}',模式串为'p_{1}p_{2}...p_{m}',当主串中第 i 个字符与模式串中第 j 个字符失配时,子串应向右滑动多远,然后与模式中的哪个字符比较?

假设此时应与模式串的第k(k<j)个字符继续比较,则模式串中前 k-1 个字符的子串必须满足下列条件,且不可能存在 k'>k 满足下列条件:
        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        p_{1}p_{2}...p_{k-1}=p_{j-k+1}p_{j-k+2}...p_{j-1}

若存在满足如上条件的子串,则发生失配时,仅需将模式串向右滑动至模式串的第k个字符和主串的第i个字符对齐,此时模式串中的前k-1 个字符的子串必定与主串中第i个字符之前长度为 k-1 的子串相等,由此,只需从模式串的第k个字符与主串的第i个字符继续比较即可,如图 4.5 所示。

当模式串已匹配相等序列中不存在满足上述条件的子串时(可视为 k=1),显然应该将模式串右移 j-1 位,让主串的第 i 个字符和模式串的第1个字符进行比较,此时右移位数最大。

当模式串的第1个字符(j=1)与主串的第i个字符发生失配时,规定 next[1]=0(可理解为将主串的第i个字符和模式串的第1个字符的前面空位置对齐,即模式串右移一位。)将模式串右移一位,从主串的下一个位置(i+1)和模式串的第1个字符继续比较。

通过上述分析可以得出 next 函数的公式:

上述公式不难理解,实际做题求 next 值时,用之前的方法也很好求,但要想用代码来实现,貌似难度还真不小,我们来尝试推理求解的科学步骤。
首先由公式可知                

next[1]=0

设 next[j]=k,此时k应满足的条件在上文中已描述。
此时 next[ j+1 ]=?可能有两种情况:
(1)若p_{k}=p_{j},则表明在模式串中

        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        p_{1}...p_{k-1}p_{k}=p_{j-k+1}...p_{j-1}p_{j}

并且不可能存在 k'>k 满足上述条件,此时 next[ j+1 ]=k+1,即 next[ j+1 ] = next[ j ]+1

(2)若p_{k}\neq p_{j},则表明在模式串中

        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        p_{1}...p_{k-1}p_{k}\neq p_{j-k+1}...p_{j-1}p_{j}

此时可将求 next 函数值的问题视为一个模式匹配的问题。用前缀 p_{1}...p_{k}去与后缀 p_{j-k+1}...p_{j}匹配,当 p_{k}\neq p_{j}时,应将p_{1}...p_{k}向右滑动至以第 next [k]个字符与p_{j}比较,若 p_{next[k]}p_{j}仍不匹配,则需要寻找长度更短的相等前后缀,下一步继续用 p_{next[next[k]]}p_{j}比较,以此类推,直到找到某个更小的 k'=next[next… [k]](1<k'<k<j),满足条件

        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        p_{1}...p_{k}=p_{j-k'+1}...p_{j}

则 next [ j+1 ]=k'+1

也可能不存在任何 k'满足上述条件,即不存在长度更短的相等前缀后缀,令 next[ j+1 ]=1

理解起来有一点费劲?下面举一个简单的例子。

图 4.6的模式串中已求得6个字符的next值,现求 next [7],因为next[ 6 ]=3,又p_{6}\neq p_{3}则需比较p_{6}p_{1 }(因next[ 3 ]=1),由于p_{6}\neq p_{1},,而next[1]=0,因此

next [7]=1;求next [8],因p_{7}=p_{1},则next [8]=next [7]+1=2;求next [9],因p_{8}=p_{2},则 next [9]=3。

通过上述分析写出求 next 值的程序如下:

void get_next(SString T,int next[]){int i=1,j=0;next[1]=0;while(i<T.length){if(j==0||T.ch[i]==T.ch[j]){++i; ++j;next[i]=j;    //若pi=pj,则 next[j+1]=next[j]+1}elsej=next[j];    //否则令j=next[j],循环继续}
}

计算机执行起来效率很高,但对于我们手工计算来说会很难。因此,当我们需要手工计算时还是用最初的方法。

与 next 数组的求解相比,KMP 的匹配算法相对要简单很多,它在形式上与简单的模式匹配算法很相似。不同之处仅在于当匹配过程产生失配时,指针i不变,指针 j退回到 next[j]的位置并重新进行比较,并且当指针j为0时,指针i和j同时加 1。即若主串的第i个位置和模式串的第1个字符不等,则应从主串的第 i+1个位置开始匹配。具体代码如下:

int Index_KMP(SString S,SString T,int next[]){int i=1,j=1;while(i<=S.length && j<=T.length){if(j==0||S.ch[i]==T.ch[j]){++i; ++j;         //继续比较后继字符}elsej=next[j];        //模式串向右移动}if(j>T.length)return i-T.length;    //匹配成功elsereturn 0;
}

[命题追踪——KMP 匹配过程中比较次数的分析]

尽管普通模式匹配的时间复杂度是 O(mn),KMP 算法的时间复杂度是 O(m+n),但在一般情况下,普通模式匹配的实际执行时间近似为 0(m+n),因此至今仍被采用。KMP 算法仅在主串与子串有很多“部分匹配”时才显得比普通算法快得多,其主要优点是主串不回溯

3.KMP算法的进一步优化

前面定义的 next 数组在某些情况下尚有缺陷,还可以进一步优化。如图 4.7 所示,模式串' aaaab '在和主串' aaabaaaab '进行匹配时:

(s1,p1等数字均为脚标)

当 i=4、j=4 时,s4跟p4(b≠a)失配,若用之前的 next 数组,则还需要进行 s4与p3、s4与p2、s4与p1这3次比较。事实上,因为p_{next[4]=3}=p_{4}=a

p_{next[3]=2}=p_{3}=a 、p_{next[2]=1}=p_{2}=a,显然后面3次用一个和p4相同的字符跟s4比较毫无意义,必然失配。那么问题出在哪里呢?

问题在于不应该出现 p_{j}=p_{next[j]}。理由是:当 p_{j}\neq s_{j}时,下次匹配必然是 p_{next[j]}s_{j}比较,若p_{j}=p_{next[j]} ,则相当于拿一个和p_{j}相等的字符跟s_{j}比较,这必然导致继续失配,这样的比较毫无意义。若出现p_{j}=p_{next[j]}则如何处理呢?

若出现p_{j}=p_{next[j]} ,则需要再次递归,将next [j] 修正为next [next [j] ],直至两者不相等为止,更新后的数组命名为 nextval。计算 next 数组修正值的算法如下,此时匹配算法不变。

void get_nextval(SString T,int nextval[]){int i=1,j=0;nextval[1]=0;while(i<T.length){if(j==0||T.ch[i]==T.ch[j]){++i; ++j;if(T.ch[i]!=T.ch[j])  nextval[i]=j;else nextval[i]=nextval[j];}elsej=nextval[j];}
}

KMP 算法对于初学者来说可能不太容易理解,读者可以尝试多读几遍本章的内容,并参考一些其他教材的相关内容来巩固这个知识点。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/830946.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Android开发知识杂录

1.XML解析问题 增加XML布局文件时候出现 mergeDebugResources 错误 解决方案 由于XML默认文件带有BOM&#xff0c;remove bom即可 2.开机启动界面添加 3.开机隐藏系统桌面 4.添加敲击传感器GPIO 1. 测试板子的GPIO引脚情况 echo in > /sys/class/gpio/gpio<gpio_number…

排序-八大排序FollowUp

FollowUp 1.插入排序 (1).直接插入排序 时间复杂度:最坏情况下:0(n^2) 最好情况下:0(n)当数据越有序 排序越快 适用于: 待排序序列 已经基本上趋于有序了! 空间复杂度:0(1) 稳定性:稳定的 public static void insertSort(int[] array){for (int i 1; i < array.length; i…

计算机网络chapter1——家庭作业

文章目录 复习题1.1节&#xff08;1&#xff09; “主机”和“端系统”之间有何不同&#xff1f;列举几种不同类型的端系统。web服务器是一种端系统吗&#xff1f;&#xff08;2&#xff09;协议一词常用来用来描述外交关系&#xff0c;维基百科是如何描述外交关系的&#xff1…

mac虚拟机软件哪个好 mac虚拟机怎么安装Windows 苹果Mac电脑上受欢迎的主流虚拟机PK Parallels Desktop和VM

什么是苹果虚拟机&#xff1f; 苹果虚拟机是一种软件工具&#xff0c;它允许在非苹果硬件上运行苹果操作系统&#xff08;如ios&#xff09;。通过使用虚拟机&#xff0c;您可以在Windows PC或Linux上体验和使用苹果的操作系统&#xff0c;而无需购买苹果硬件。 如何使用苹果虚…

CSDN如何在个人主页开启自定义模块|微信公众号

目前只有下面三种身份才具有这个功能。 VIP博客专家企业博客 栏目内容不知道怎么写HTML的&#xff0c;可以联系我帮你添加

Maven入门:1.简介与环境搭建

一.简介与环境搭建 1.Maven&#xff1a;用于自动化构建项目&#xff08;按照企业主流模板构建完善的项目结构&#xff09;和管理项目依赖&#xff08;依赖就是项目的jar包&#xff0c;通过配置的方式进行添加和管理&#xff0c;自动下载和导入&#xff09;的工具。即更加方便构…

C 408—《数据结构》图、查找、排序专题考点(含解析)

目录 Δ前言 六、图 6.1 图的基本概念 6.2 图的存储及基本操作 6.3 图的遍历 6.4 图的应用 七、查找 7.2 顺序查找和折半查找 7.3 树型查找 7.4 B树和B树 7.5 散列表 八、排序 8.2 插入排序 8.3 交换排序 8.4 选择排序 8.5 归并排序和基数排序 8.6 各种内部排序算法的比较及…

表格中斜线的处理

此处的斜线,不是用表格写的,但是也适用于表格,只是需要更改表格的样式,可以 按照如下处理,即可 呈现的效果:如图所示 template部分: <div class"header_detail custom"><div class"right">节次</div><div class"left">…

C/C++实现高性能并行计算——1.pthreads并行编程(中)

系列文章目录 pthreads并行编程(上)pthreads并行编程(中)pthreads并行编程(下)使用OpenMP进行共享内存编程 文章目录 系列文章目录前言一、临界区1.1 pi值估计的例子1.2 找到问题竞争条件临界区 二、忙等待三、互斥量3.1 定义和初始化互斥锁3.2 销毁。3.3 获得临界区的访问权&…

windows11安装nginx

1.解压nginx安装包到没有中文的目录 2.双击运行nginx.exe 3.任务管理器查看是否有nginx进程 4.任务管理器->性能->资源监视器 5.网络->侦听端口&#xff0c;查看nginx侦听的端口&#xff0c;这里是90端口

大连宇都环境 | 成都5月水科技大会暨技术装备成果展览会

中华环保联合会水环境治理专业委员会 秘书处 王小雅 13718793867 —— 展位号&#xff1a;A09 —— 一、企业介绍 大连宇都环境成立于2002年&#xff0c;公司20年 MBBR填料产品及工艺技术&#xff0c;&#xff0c;构建了研发、制造、设计、工程、运营链式服务能力&#xff…

数据赋能(73)——数据要素:特征

生产要素中的数据要素具有一系列基本特征&#xff0c;这些特征使得数据在现代经济活动中发挥着越来越重要的作用。数据要素的主要特征如下图所示。 数据已经成为关键的生产要素&#xff0c;数据要素的基本特征可以概括为&#xff1a;虚拟性、非消耗性、非稀缺性、非均质性、排他…

selinux 基础知识

目录 概念 作用 SELinux与传统的权限区别 SELinux工作原理 名词解释 主体&#xff08;Subject&#xff09; 目标&#xff08;Object&#xff09; 策略&#xff08;Policy&#xff09; 安全上下文&#xff08;Security Context&#xff09; 文件安全上下文查看 先启用…

如何解决网络应用运行中的审核问题【系列研究预告】

目前互联网是非常发达的&#xff0c;但是随着技术的发展&#xff0c;有些问题逐渐变得严重。对于一般企业而言&#xff0c;一个比较重要的问题就是审核准确性和成本问题。 比如知乎的审判官&#xff0c;我本人是最早的一批审判官&#xff0c;然而多年下来的经历却很让人感到无…

数据结构—C语言实现双向链表

目录 1.双向带头循环链表 2.自定义头文件&#xff1a; 3.List.cpp 文件 3.1 newnode()函数讲解 3.2 init() 函数 初始化 3.3 pushback()函数 尾插 3.4 pushfront()函数 头插 3.5 popback() 尾删 3.6 popfront() 函数 头删 3.7 insert()函数 在pos之后插入 3.8 popbac…

uniapp 对接 Apple 登录

由于苹果要求App使用第三方登录必须要求接入Apple登录 不然审核不过 所以&#xff1a; 一、勾选苹果登录 二、 设置AppId Sign In Apple 设置完成重新生成描述文件 &#xff01;&#xff01;&#xff01;&#xff01;证书没关系 示例代码&#xff1a; async appleLogin…

Delta lake with Java--将数据保存到Minio

今天看了之前发的文章&#xff0c;居然有1条评论&#xff0c;看到我写的东西还是有点用。 今天要解决的问题是如何将 Delta产生的数据保存到Minio里面。 1、安装Minio&#xff0c;去官网下载最新版本的Minio&#xff0c;进入下载目录&#xff0c;运行如下命令&#xff0c;曾经…

Co-assistant Networks for Label Correction论文速读

文章目录 Co-assistant Networks for Label Correction摘要方法Noise DetectorNoise Cleaner损失函数 实验结果 Co-assistant Networks for Label Correction 摘要 问题描述&#xff1a; 描述医学图像数据集中存在损坏标签的问题。强调损坏标签对深度神经网络性能的影响。 提…

SpringBoot指标监控

一.SpringBoot指标监控_添加Actuator功能 Spring Boot Actuator可以帮助程序员监控和管理SpringBoot应用&#xff0c;比如健康检查、内存使用情况统计、线程使用情况统计等。我 们在SpringBoot项目中添加Actuator功能&#xff0c;即可使用Actuator监控 项目&#xff0c;用法如…

使用YALMIP定义LMI,SEDUMI求解矩阵方程

YALMIP&#xff08;Yet Another MATLAB Package for Modeling and Optimization&#xff09;是一个MATLAB工具箱&#xff0c;用于定义和求解优化问题&#xff0c;包括线性矩阵不等式&#xff08;LMI&#xff09;问题。SEDUMI是一个用于求解LMI问题的求解器&#xff0c;它可以与…