python数据结构教程第二课
字符串是计算机数据处理过程中最重要,也是最基础的内容,学好它会帮助我们的编程更加高效简洁
一.简介
二.字符串的抽象数据类型
三.字符串的匹配算法
1 朴素匹配算法
2 KMP匹配算法
四.字符串类的链表实现
五.正则表达式
1.什么是正则表达式
2.python的正则表达式
在计算机领域中,基本的文字符号称为字符,符号的序列称为字符串,有穷的一组字符构成的集合称为字符集,在实际中经常使用的字符集有ASCII字符集或者Unicode字符集,python2.5以后默认的字符集就是ASCII字符集。
一个字符串类型应当有的方法有切片,按位取元素、返回字符串长度、字符串拼接等,其基本ADT如下:
ADT String:
String(self,sseq) #基于字符序列sseq建立一个字符串
is_empty(self) #判断本字符串是否空串
len(self) #返回字符串长度
char(self,index) #返回位于index位置的字符
substr(self,a,b) #取得位置在[a,b]区间的子字符串
match(self,str) #查找str在串中出现的第一个位置
concat(self,str) #将本字符串与str拼接成新串
subst(self,str1,str2) #将字符串里的str1替换为str2
上列match(self,str)中,self被称为目标串,str被称为模式串。
这里要提及的是,在python内部已经有了较为完善的字符串类型str,所以本篇介绍的重点不在于字符串本身类型的实现,而在与基于字符串类型的重要操作和算法,接下来会列出python中str类型的一些常用方法,然后依次介绍字符串的重要操作——字符串匹配,字符串类型的链表实现以及进阶内容——正则表达式。
str类的常用方法如下:
str1 = str('hello world!') #建立新串
str2 = str1[0:6] #从旧串切片或的新串
str3 = str2 + str1 #两个串的连接
for char in str3: #支持yield操作
print(char, end = '') #输出str3
print() #换行
print(len(str3)) #取字符串长度
stra = '**##'
strb = '1111'
strc = strb.join(stra) #插入操作,stra为分隔符
print(strc)
上列代码的输出为:
hello hello world!
18
*1111*1111#1111#
字符子串匹配是字符串操作中非常重要的一个,同时它也是很多更高级字符串操作的基础,这里介绍两种,一种是基本的、通俗的朴素匹配算法,另一种是高级的KMP算法
1.朴素匹配算法
朴素匹配算法采用最直观可行的策略:
1)使用模式串与目标串从左到右逐个字符匹配
2)发现不匹配时,转去考虑目标串的下一个位置是否与模式串匹配
3)匹配成功则结束算法
朴素匹配算法非常简单,容易理解,但其效率很低,这里直接给出匹配算法
def naive_matching(t,p):
m,n = len(p),len(t)
i,j = 0,0
while i < m and j < n: #i==m说明找到匹配
if p[i] == t[j]: #字符相同则考虑下一对字符
i,j = i+1,j+1
else: #字符不同则考虑t中的下一位置
i,j = 0,j-i+1
if i == m: #找到匹配,返回位置
return j - i
return -1 #无匹配值,返回-1
仔细分析上面的代码,可以发现其效率低下的原因在于执行中可能出现回朔,即:匹配中一旦遇到不同,模式串就回到目标串中前面的下一个位置,从那里再次从头开始比较字符。这相当于把每次的字符比较看成完全独立的操作,完全没有利用字符串本身的特点,也没有充分利用前面已经做过的字符比较中得到的信息,这里再介绍一种高效的匹配算法
2.KMP匹配算法
与朴素匹配算法相比,KMP匹配算法的效率又了本质的提高,其主要优势在于,克服了朴素匹配算法中的回朔现象。我们可以这么考虑:当一次匹配失败之后,模式串并不返回目标串的前面从下一位置重新开始,而是停留在失败的位置i,尝试模式串前面某一位置k与目标串的这一位置继续进行比较,这样就可以充分利用上一次比较的信息
模式串中每一个位置i的元素都对应着自己失败后应该跳转的位置k,我们称这个表为pnext,同时应当指出的是,pnext与目标串无关,可以依靠模式串本身的信息建立
现在假设我们已有了pnext表,给出KMP算法的函数代码:
def matching_KMP(t,p,pnext):
j,i = 0,0
n,m = len(t),len(p)
while j < n and i < m: #i==m说明找到匹配
if i == -1 or t[j] == p[i]:
j,i = j+1,i+1 #比较下一字符
else: #失败则根据pnext表查找模式串跳转位置
i = pnext[i]
if i == m: #找到匹配,返回下标
return j-i
return -1 #无匹配,返回-1
上列代码第五行,判断条件有 i == -1是因为,在一些匹配失败的情况下,可能存在匹配失败的字符之前所做的匹配都不包含利用价值,这种情况就需要从头开始,因此在pnext[0]中存入 -1
接下来,我们讲述pnext表的构建
这里先给出前缀、后缀的定义:
假设有串‘abcab’
则将‘a’、‘ab’、‘abc’、‘abca’称为母串的前缀
又将‘b’、‘ab’、‘cab’、‘bcab’称为母串的后缀
模式串中位置k匹配失败后,调转的位置取决于前k-1个字符中,最长相等前后缀的大小,如上‘abcdabcgd’建立的pnext表为:[-1, 0, 0, 0, -1, 0, 0, 3, 0]
这里使用递推的思想求解pnext表:
1)当位于k的元素值与位于i的元素值相等时,如果k+1等于i+1则我们可以知道k+1对应的跳转位置就为i+1的跳转位置,如果k+1不等于i+1,则k+1的跳转位置对应于i+1
2)当位于k的元素值与位于i的元素值不相等时,i的值跳转到pnext[i]
3)k为匹配错误位置,i为前k-1个元素中的最大相等前后缀长度,pnext[0] = -1
由上面的步骤我们可以得到pnext的求解函数
def gen_pnext(p):
k,i,m = 0,-1,len(p)
pnext = [-1]*m #所有初始值设为-1
while k < m-1:
if i == -1 or p[k] == p[i]: #
i,k = i+1,k+1
if p[k] == p[i]:
pnext[k] = pnext[i]
else:
pnext[k] = i
else:
i = pnext[i]
return pnext
下面使用KMP算法举个例子:
stra = 'hello! this is KMP_matching'
strb = 'this'
pnext = gen_pnext(strb)
k = matching_KMP(stra,strb,pnext)
print(stra)
print(strb)
print('\"'+strb+'\"'+' in '+'\"'+stra+'\"' +'\'s ' + str(k))
结果为:
hello! this is KMP_matching
this
"this" in "hello! this is KMP_matching"'s 7
利用单链表实现字符串类可以更方便的实现字符串的插入删除操作,接下来给出使用链表实现的字符串类源码,包含了字符串类的一些基本方法:
import copy #导入复制库
#链表的结点类
class Node:
def __init__(self,char,next_ = None):
self.char = char
self.next = next_
'''字符串的链表类构造,实现了字符串的初始化、输出、求长度、朴素匹配算法、按位置返回字符、子串替换、KMP匹配算法、字符移除等方法'''
class strllist:
def __init__(self,string = None): #初始化
self.head = Node()
if string is None:
self.num = 0
elif string =='':
self.head.char =''
self.num = 1
else:
self.head.char = string[0]
p = self.head
i = 1
while i < len(string):
t = Node(string[i])
p.next = t
p = p.next
i += 1
self.num = len(string)
def __len__(self): #返回串长度
return self.num
#朴素匹配算法的链表实现
def naive_matching(self,string):
p = self.head
q = self.head
m = len(string)
i = 0
k = 1
while p is not None and i < m:
if p.char == string[i]:
p = p.next
i += 1
else:
i = 0
q = q.next
p = q
k += 1
if i == m:
return k
else:
return -1
#返回第i个字符
def go_loc(self,i):
k = 1
p = self.head
while k < i:
p = p.next
k += 1
return p
#将字符串里的stra替换为strb
def replace(self,stra,strb):
while self.naive_matching(stra) != -1:
i = self.naive_matching(stra)
if i == 1:
p = self.go_loc(len(stra)+1)
bllist = strllist(strb)
self.head = bllist.head
q = self.go_loc(len(strb))
q.next = p
else:
p = self.go_loc(i-1)
q = self.go_loc(i+len(stra))
bllist = strllist(strb)
p.next = bllist.head
t = bllist.go_loc(len(strb))
t.next = q
#字符串输出
def printall(self):
p = self.head
while p is not None:
print(p.char,end = '')
p = p.next
print()
#静态方法,求串的pnext表
@staticmethod
def gen_pnext(p):
m = len(p)
pnext = [-1] * m
i,k = 0,-1
while i < m-1:
if k == -1 or p[i] == p[k]:
i,k = i+1,k+1
if p[i] == p[k]:
pnext[i] = pnext[k]
else:pnext[i] = k
else:
k = pnext[k]
return pnext
#KMP匹配算法
def matching_KMP(self,string,pnext):
p = self.head
m = len(string)
i = 0
k = 1
while p is not None and i < m:
if i == -1 or p.char == string[i]:
i += 1
p = p.next
k += 1
else:
i = pnext[i]
if i == m:
return k - i
return -1
#移除串里的stra
def remove(self,stra):
while self.naive_matching(stra) != -1:
i = self.naive_matching(stra)
if i == 1:
self.head = self.go_loc(len(stra)+1)
else:
p = self.go_loc(i-1)
q = self.go_loc(i+len(stra))
p.next = q
举例:
a = strllist('abcdefg abcdefg')
b = str('a')
a.printall()
a.remove(b)
a.printall()
结果:
abcdefg abcdefg
bcdefg bcdefg
1.什么是正则表达式
正则表达式,又称为规则表达式(Regular Expression,常简写为regex、regexp或RE),正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。
给定一个正则表达式和另一个字符串,我们可以达到如下的目的:
2.python的正则表达式
python的正则表达式包re规定了一组特殊字符,称为元字符,在匹配字符串的时候,它们起着特殊的作用,元字符一共有14个:
. ^ $ * + ? \ | { } [ ] ( )
在普通字符串里除了转义字符 \ 其余的都是普通字符,只有在它们出现在re包提供的一些特殊操作时,这些字符才有特殊意义
python re包的主要操作:
#生成正则表达式对象
r1 = re.complie(pattern, flag = 0)
#在string里检索pattern对象
re.search(pattern, string, flag = 0)
#检查string里是否存在与pattern匹配的前缀
re.match(pattern,string,flag = 0)
'''以pattern为分隔符将string分段,maxsplit指明最大分割数,flags = 0表示处理完整个string'''
re.split(pattern, string,maxsplit=0,flags = 0)
先前介绍了python re包里的一些元字符,接下来将具体介绍各种元字符的意义
1)字符组描述符 […]:
表示与方括号中列出的任一字符匹配,字符的排序不重要
[abc] 可与 a 或 b 或 c 匹配
[0-9a-zA-Z] 可匹配所有数字和字母
a[1-9][0-9] 可匹配a10,a11,a12…a99
[^…] 表示对^之后的字符组求补
2)圆点字符 . :
圆点字符是通配符,可以匹配任意字符
a…b 可以匹配以a开头以b结尾的任意4字符
3)转义字符 \ :
转义字符定义了一些常用的字符组,如:
\d 与十进制数字匹配
\D 与非十进制的所有字符匹配
\s 与空白字符匹配
\S 与非空白字符匹配
\w 与字母数字符匹配
4)重复描述符 :
模式 a 要求该匹配模式可以匹配a的0次或任意多次重复
如:
import re
re.split('a*','abbaaabbbddbbabbaddaaaddaa')
得到:
['', 'bb', 'bbbddbb', 'bb', 'dd', 'dd', '']
5)可选描述符 ?:
模式a? 可以与空串或与a匹配,如:
-?\d+可表示所有整数
6) 确定次数重复 {n}:
a{n} 与a的n次重复匹配
7)重复次数范围描述符 {m,n}:
a{m,n}可以与a的m次到n次重复匹配
8)选择描述符 |:
a|b|c表示可与a或b或c中的任意一个匹配
9)首位描述符
行首描述符:‘^a’只能与位于行首的前缀子串a匹配
行尾描述符: ‘$a’只能与位于行尾的后缀子串a匹配
串首描述符:‘\A’开头的模式只能与整个被匹配串的前缀匹配
串尾描述符: ‘\Z’结束的模式只能与整个被匹配串的后缀匹配