c语言字符串删除子串_数据结构与算法之7——字符串

字符串的一些相关概念

  • 字符串长度:一串字符串里字符的个数,长度为0的串称为空串。
  • 与一般的线性表类似,字符串里的字符依次排列,一个串里的字符串有其固定的下标位置,字符串里首个字符的下标为0。
  • 字符串相等:不仅是两串字符串长度相等,字符串中每个字符都在对应的位置相等,“=”
  • 字典序:对于字符串s1=a0a1a2...,s2=b0b1b2....如果存在k>=0,使得对于任何i<k,都有ai=bi,但是ak<bk,则s1<s2。简单来说就是两个串相同下标的各对字符相同,但其中一个字符串较短,那么认为它较小。
  • 字符串拼接:python中用“+”来表示。例如s1+s2=a0a1a2...b0b1b2...。
  • 子串关系:如果s1与s2中某个连续的片段相同,则称s1为s2的子串,同样也可以说s1在s2中出现,s1和s2相同字符段的首个字符位置即s1在s2出现的位置。当然,在s2中可能出现多个与s1相同的字符段,称为s1在s2中多次出现
  • 前缀和后缀是两种特殊子串,前缀是字符串开头任意一段字符组成的子串,后缀是字符串结尾任意一段字符组成的子串。
  • 串s的n次幂表示,连续的n个字符串s拼接而成的串。如s=abc,s^3=abcabcabc

字符串的实现问题

  • 字符串内容的存储:有2种极端的方式:(1)把一个字符串的全部内容存储在一块连续存储的区域。(2)把串中每个字符单独存入一个独立存储块,并将这些块链接起来。这两种方式都会带来存储问题,前者需要大块的存储区,极长的字符串可能带来问题。而后者需要附加额外的链接域,额外存储的开销较大。这里选择了折中的方式:把一个串的字符序列分段保存在一组存储块里,并链接起这些存储块。
  • 串结束的表示:(1)用一个数据域记录字符串的长度,就像连续表中的num域。(2)用一个特殊编码表示串结束,为此需要保证该编码不代表任何字符。C语言中采用了第2种方式
  • 字符串替换的问题:把主串s中的子串t替换为目标串t',这里会有几个问题:
    • 被替换的t可能在s中多次出现,需要通过一系列具体的子串代换完成整个替换。
    • t在s中多次出现可能重叠,只能规定一种代换顺序(例如从左到右),被一个具体代换破坏的子串不应再带入新串。
    • 在完成一次子串代换后,应该从代入的新串之后继续工作,即使代入新串后形成的部分中存在与t匹配的片段,也不应该在本次串替换中考虑。
    • 无法预测t在s中几次独立出现,以及目标字符串t'的具体长度,因此构造替换字符串过程中可能需要扩充存储。

python中的字符串

python中的str是字符串的一种实现方式,str是不可变对象,一旦创建其内容和长度不变化。

18089507e7e30de67ede390c1f68641e.png

python中字符串的存储方式和一体式顺序表类似,有记录表的长度等信息以及具体内容存储

str操作:(1)获取str的信息,如串长,检查字符串内容是否全为数字等。(2)基于已有的str对象构造新的str对象,例如切片,构造大小写,各种格式化操作等。切分操作split是构造包含多个字符串的表,更复杂的操作如replace。如count可以检查子串出现次数,endwith检查后缀,find/index找子串的位置等。

求串长度和定位访问字符都是o(1)操作,但是其他操作都需要扫描整个字符串,如in,not,min/max等,各种字符串类型的判断,这些都需要循环整个字符串,为o(n)。

字符串匹配查找

又称为模式匹配,假设有2个字符串t=t1,t2,t3...tn,p=p0.p1.p2...pn。字符串匹配就是在t中查找与p相同的子串的操作,t称为目标串(被匹配),p为模式串。模式串的长度一般远小于目标串的长度。

串匹配和朴素匹配算法

从目标串的某个位置i开始,模式串里的每个字符都与目标串里的对应字符相同,就是找到了一个匹配。串匹配算法设计的关键:(1)怎样选择开始比较的字符对;(2)发现了不匹配后,该怎么办?

朴素匹配算法:(1)从左到右逐个字符匹配;(2)发现不匹配时,转去考虑目标串里的下一个位置是否与模式串匹配。该算法的时间复杂度为o(mxn),m,n为目标字符串和模式字符串长度。

0c00467f3ce636ac57a469efb4251850.png
def  

无回溯串匹配算法(KMP算法)

9d07a7655b6fe089eca6a104f00f6eac.png

KMP算法的基本想法是匹配中的不回溯。如果匹配中用模式串里的

匹配某个
时失败了,就找到某个特定的
(
),下一步用模式串中字符
与目标串中的
比较,也就是说,
在匹配失败时把模式串中前移若干位置,用模式串里匹配失败字符之前的某个字符与目标串匹配失败的字符比较

KMP算法的关键认识是:在

匹配失败时,所有的
(
)都已匹配成功,也就是目标串
之前的i个字符与模式串
的前i个字符
匹配成功。那么决定模式串前移位置的话,只需要分析模式串p本身的结构就可以决定,通过对模式串的分析以后,对任意的目标串进行匹配时,都采取相同的匹配策略。

那么这里可以得出,对于p中的每个i,都有与之对应的下标

,与目标匹配串无关,这里的
是指,当模式串在i的位置(
)与目标串
匹配失败时,就将模式串
的位置与目标串
继续匹配,即决定了前进的步数。那么可以将模式串的每个i作为index,对应的
作为内容构建一个pnext表,用表元素pnext[i]表示i对应的
值。如果在匹配位置i发生匹配失败时,但是发现i之前的所有匹配都没有价值,则下次直接从
开始匹配,那么这里的pnext[i]取-1,显然,pnext[0]=-1。
def 

循环不会多与o(n),算法复杂度为o(n),目标字符串长度为n。

构造pnext表

  • 模式串移动之后,作为下一个用于匹配的字符串新位置
    ,其前缀子串
    应该与匹配失败的字符串之前同样长度的子串相同。
  • 如果匹配在模式串的位置i失败时,而位置i的前缀子串中满足上述条件的位置不止一处,那么只能做最短的移动,将模式串移到最近的那个满足条件的位置,以保证不遗漏可能的匹配。

1e1cec5a83e5c951c451a64a3e75b190.png

我们仔细分析一下这里的pnext表是如何构造的。如上图(1)所示,目标串中位置j之前的i个字符也就是模式串的前i个字符,也就是说目标串中的子串

就是
。那么现在要找一个k,下次匹配用
与前面匹配失败的
比较,也就是将模式串移到
相对应的位置。那如图(2)所示,
前的字符串
应该与目标串
匹配。这样确定k的问题就变成了确定
的相等前缀和后缀的长度
。前面说过移动距离应该尽可能的短,这样可以保证匹配不会出现遗漏,那么反之,k的值应该尽可能大,那么就是
的最长相等前缀和后缀。如果
最长相等前后缀的长度为k,在
时,模式串就应该前移i-k位,pnext[i]=k。

递推计算最长相等前后缀的长度

ee6477f97ef0dcde0c2ef6a4b5fd8089.png

假设现在要对子串

递推计算最长相等前后缀的长度,这时对pnext[i-1]已经计算出结果为k-1,那么有两种情况:
  1. 如果
    ,对于i的最长相等前后缀,比对i-1的最长相等前后缀的长度多1,由此可将pnext[i]=k,然后考虑下个字符。
  2. 否则就应该把
    的最长相等前缀移过来继续检查。(
    的最长相等前缀也是
    的相等前缀,因此继续检查是合法的。)

已知pnext[0]=-1和直至pnext[i-1]的已有值求pnext[i]的算法:

  1. 假设pnext[i-1]=k-1,如果
    ,那么
    的最长相等前后缀的长度就是k,将其记入pnext[i],将i值+1后继续。
  2. 如果
    ,就将k设置为pnext[k]的值,(转去考虑前一个更短的保证匹配的前缀,基于它继续检查)。
  3. 如果k的值为-1,这个值来自于第(2)步中的pnext,那么
    的最长相同前后缀长度为0,设置pnext[i]=0,将i值+1后继续。
def 

KMP算法的时间复杂度

设模式串和目标串的长度为m和n,KMP算法的时间复杂度为o(m+n),因为多数情况下m远小于n,可以认为算法复杂度为o(n),显然由于朴素匹配算法的o(mxn)。KMP算法最适合的场景在于一个模式串需要反复在一个或多个目标串里匹配,因为pnext表只需要做一次,但是可以使用多次。

正则表达式(划重点)

  • 任一字符仅仅与其本身匹配。
  • 圆点符号'·'可以匹配任意字符。
  • 符号'^'只匹配一行目标串的开头,不匹配任何具体字符(表示从字符串开头开始匹配)
  • 符号'$'表示匹配到一行的目标字符串的结束,不匹任何具体字符。(表示匹配到字符串结尾)
  • 符号'A'表示与整个被匹配串的前缀匹配,'Z'表示与整个被匹配串的后缀匹配。
  • 符号'*'表示前面的字符可以匹配0个或任意多个相同字符。
  • 字符组描述符:[0-9]表示匹配任意十进制字符,[a-zA-Z]表示匹配任意大小写字母字符,[0-9a-zA-Z]表示匹配任意数字和字母。[^...]表示取补集,匹配未在中括号中出现的字符,[^0-9]匹配任意非十进制数字字符。
  • 转义字符:d:与十进制数字匹配,等价于[0-9];D:与非十进制数字字符匹配,等价于[^0-9];s:与所有空白字符匹配,等价于[ tvnfr];S:与所有非空白字符匹配;w:与所有字母数字字符匹配,等价于[0-9a-zA-Z];W:与所有非字母数字字符匹配,等价于[^0-9a-zA-Z];
  • 重复描述符:'*'匹配0个或多个,'+'匹配1个或多个等,这些匹配会带来一个问题:
    • 贪婪匹配:模式与字符串里有可能匹配的最长子串匹配。re中'*'默认为贪婪匹配。
    • 非贪婪匹配(吝啬匹配):模式与有可能匹配的最短子串匹配。对所有重复模式描述符,python都提供了相应的吝啬匹配描述符。
    • 非贪婪匹配描述符:与各种贪婪匹配描述符相对应,'*?','+?','??','{m,n}?'。表示可以执行的最短匹配。
  • 可选描述符:'?',代表匹配钱一个字符0次或1次。
  • 重复次数描述符:确定重复的次数用{n}描述,a{n}表示与a匹配的串的n次重复匹配。
  • 重复次数的范围描述符:{m,n}表示m到n次的重复匹配。
  • 选择描述符:'|',a|b|c表示匹配a或b或c,[abc]等价。
  • 单词边界:'b'描述单词边界,它在实际单词边界位置匹配空串(不匹配实际字符串),单词是数字和字母的排列,其边界就是非字母数字的字符或者无字符(串的开头/结束)。由于python中'b'表示退格符,因此要写出'b'或者r'b',比如r'b123b',能匹配(123,123)但不能匹配abc123ab。转义符'B'是'b'的补,要求相应位置必须是数字或字母,即能匹配abc123ab,不能匹配(123,123)。

匹配对象(match对象)

  • match1=re.search(pt,text),(返回bool值),if match1:.....
  • 取得被匹配的字符串:match1.group(),得到匹配成功的字串。
  • 目标串里的匹配位置:match1.start(),,在目标串匹配成功字符串第一个字符的位置。
  • 目标串里被匹配子串的结束位置:match1.end(),可以这样表示,match1.group()=text[match1.start():match1.end()]。
  • 目标串里被匹配的区间:match1.span(),得到匹配开始和结束位置的二元组。
  • match1.re,match1.string,取得match1对象所做匹配的正则表达式对象和目标串。

模式里的组(group)

在模式串中用()括起来的地方确定了一个被匹配的组,在一次成功的匹配时,模式串里的各个组都成功匹配,与其匹配的字符串组被编号为1,2,3...,后面可以通过调用match1.group(n)。

举个栗子:

match1=re.search('.((.)e)f','abcdef'),表达式match1.group()的值是('de','d'),match1.group(1)的值是'de',而match1.group(2)的值是'd'。

组还有一个重要的功能是应用前面的匹配,建立前后匹配之间的约束关系,可以在模式串的后面部分用n的形式引用。表示要求在该位置匹配同一子串,这里的n是一个表示组序号的整数。例如r'(.(2)) 1'可以匹配ok ok,但是不能匹配ok ko。

其他的匹配操作

  • re.fullmatch(pattern,string,flags=0),如果整个目标串与pattern匹配成功,就返回记录匹配成功的信息的match对象,不成功返回None。
  • re.finditer(pattern,string,flags=0),与re.findall()类似,但是不是返回一个表,而是返回一个迭代器,可以像其他迭代器已一样使用,例如for循环迭代。
  • re.sub(pattern,repl,string,count=0,flags=0),本方法生成替换结果的串,其中string里与pattern匹配的各非重叠子串顺序用另一个参数repl代换。

正则表达式的使用

在一些情况下,目标串里可能存在一些(可能很多)与正则表达式匹配的子串,需要逐个处理,采用匹配迭代器的方式最方便:

rel

参考书籍:《数据结构与算法—python语言描述》—裘宗燕

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值