串和广义表(python实现)
作者:互联网
字符串和广义表
一、 字符串
1.1 定义
串也称为字符串,是由零个或者多个字符组成的有限序列。串仅由字符组成,记作:
\(S\)="\(a_1a_2...a_n\)"
其中,\(S\) 是串名,双引号括起来的字符序列 \(a_1a_2…a_n\) 是串值,\(n\) 表示串的长度
1.2 基本概念
- 索引: 一个字符在串中的位置称为该字符在串中的索引,用整数表示,约定首字的索引为0。另外,当在字符串中检索某个字符或某个字符串时,用-1表示没有找到该字符或者字符串;
- 子串: 串 S 中任意个连续的字符组成的子序列 Sub 称为该串的子串,S 称为 Sub 的主串;
- 串相等: 两个串相等的条件是两个串的长度相等,且串中各对应位置的字符均相等;
- 比较串大小:两个串的大小由对应位置的首个不同字符的大小决定,字符比较次序是从头开始依次向后。另外,当两个串长度不等而对应位置的字符都相同时,较长的串比较大。
1.3 串的模式匹配算法
问题: 模式匹配就是有两个字符串,分别是串S和串P,其串S称为目标串,串P称为模式串,如果在目标串中查找到模式串,则称为模式匹成功,返回子串的第一个字符在目标串出现的位置。如果在目标串中未查找到模式串,则称模式匹配失败,返回 -1.
方法:
- Brute-Force 算法:也成暴力搜索算法
- KMP算法
Brute-Force 算法
Brute-Force算法用来实现串的朴素模式匹配,是最简单的一种模式匹配算法,简称BF算法。
- 算法思想
从目标串 \(S=\)"\(s_0s_1…s_{n-1}\)" 的第一个字符开始,与模式串 \(P==\)"\(p_0p_1…p_{m-1}\)" 的第一个字符进行比较:
-
若相等,则继续逐个比较后续字符
-
若不相等,则从目标串的下一个字符起重新和模式串的字符进行比较
以此类推,直至模式串 \(P\) 中的每个字符依次和目标串 \(S\) 中的一个连续的字符序列的相应字符都相等,则称匹配成功,返回和模式串P中第一个字符相等的字符在目标串S中的序号;否则说明模式串P不是目标串的子串,匹配不成功,返回-1。
- 算法分析
例如,目标串 \(S=\)"\(abcdeabcdf\)",模式串 \(P=\)"\(abcdf\)",判断模式串\(P\) 与目标串 $ S $ 是否匹配,根据Brute-Force算法的思想分析匹配的过程如下。
假设 $ i $ 为目标串 \(S\) 的当前下标索引,$j $为模式串 $ P$ 的当前下标索引,默认 \(i、j\) 的初始值为 \(0\)。
第一次匹配,从 \(i=0、j=0\) 开始匹配,当 $j=4、i=\(4 时,匹配失败。因此,要将\) i $回溯到 \(i=1,j=0\),如图所示:
第二次匹配,从\(i=1、j=0\)开始匹配,不难发现此时匹配失败,如图所示。因此,要修改$ i、j $的值,重新开始匹配,从 \(i=2、j=0\) 开始。
第三次匹配,\(S[i=2]!=P[j=0]\),如下图所示。第三次匹配结束,修改 $i $ 的值,\(i=3\)。
第四次匹配,\(S[i=3]!=P[j=0]\),如下图所示。第四次匹配结束,修改 $i $ 的值,\(i=4\)。
第五次匹配,\(S[i=4]!=P[j=0]\),如下图所示。第五次匹配结束,修改 \(i\) 的值,\(i=5\):
第六次匹配,从$ i=5、j=0 $开始匹配,当 $i=9、j=4 $时匹配成功,如下图所示:
从上面的分析可以得到,若 \(m\) 为目标串长度,$n $为模式串长度,则 Brute-Force 算法在匹配时所花费的时间分为以下两种情况来分析。:
- 最好的情况下,第一次就匹配成功,目标串与模式串匹配,比较次数为模式串的长度 $ n$,时间复杂度为 \(O(n)\)。
- 最坏情况下,每次匹配比较至模式串的最后一个字符又失败,并且比较了目标串中所有长度为 $n $ 的子串,时间复杂度为\(O(n×(n-m+1)=O(n×m)\)。
- 代码实现
def BF(S1, S2):
# 字符串S1的索引,从 0 开始
i = 0
# 字符串S2的索引,从 0 开始
j = 0
while i < len(S1) and j < len(S2):
if S1[i] == S2[j]:
j += 1
i += 1
# S1[i] != S2[j] , 将指针回溯
else:
i = i - j + 1
j = 0
# 如果在S1中找到字符串 S2,则返回S2首字符在S1中的下标索引
if j == len(S2):
index= i - len(S2)
else:
index = -1
return index
KMP算法
- 算法思想
从目标串 \(S\) 的第一个字符开始扫描,逐一与模式串 $P $ 对应的字符进行匹配:
-
若该组字符匹配,则继续匹配下一组字符;
-
若该组字符不匹配,则并不是简单地从目标串下一个字符开始新一轮的匹配,而是通过一个前缀数组跳过不必要匹配的目标串字符,以达到优化效果。
从KMP的算法思想中可以得到两个信息:
一是前缀数组是什么以及怎么构建前缀数组,
二是在得到前缀数组后怎么利用它达到优化的效果。
- 前缀数组
前缀、后缀的概念:
- 前缀:前缀就是除了最后一个字符以外,一个字符串的全部头部集合。例如,有一个字符串 \(S=\)"\(abc\)",其前缀为{"\(a\)","\(ab\)"};
- 后缀:后缀就是指除了第一个字符以外,一个字符串的全部尾部集合。例如,有一个字符串 \(S=\)"\(abc\)",其后缀为{"\(c\)","\(bc\)"}。
在这里要注意,从中间位置截取的一段字符串是不能被称为前缀或后缀的。例如,字符串 \(S=\)"\(abcd\)",字符串 "\(bc\)" 不属于前缀数组或者后缀数组。
下面通过一个例子来讲解如何构建前缀数组。现在有一个字符串S="\(bfbfbfkmpbf\)"。
字符串 "\(b\)" 的前缀和后缀都为空集,最长共有元素长度为0。
字符串"\(bf\)"的前缀为{"\(b\)"},后缀为{"\(f\)"},没有相同的前缀子串和后缀子串,最长共有元素长度为0。
字符串"\(bfb\)"的前缀为{"\(b\)","\(bf\)"},后缀为{"\(b\)","\(fb\)"},相同的前缀子串和后缀子串为"\(b\)",最长共有元素长度为1。
字符串"$bfbf$"的前缀为{"$b$","$bf$","$bfb$"},后缀为{"$f$","$bf$","$fbf$"},相同的前缀子串和后缀子串为"$bf$",最长共有元素长度为2。
字符串"\(bfbfb\)"的前缀为{"\(b\)","\(bf\)","\(bfb\)","\(bfbf\)"},后缀为{"\(b\)","\(fb\)","\(bfb\)","\(fbfb\)"},相同的前缀子串和后缀子串为"\(bfb\)",最长共有元素长度为3。
以此类推......
基于上述分析,可以获得一个前缀数组 \(prefix=\{0,0,1,2,3,4,0,0,0,1,2\}\)。为了方便后面应用KMP算法进行计算,将前缀数组的第一个位置的元素置为 -1,将当前前缀数组的元素都往后移动一个位置,将最后一个位置的元素删除,得到一个新的前缀数组,\(prefix=\{-1,0,0,1,2,3,4,0,0,0,1\}\)
- 算法分析
由上可知如何构建一个前缀数组prefix,现在来分析KMP算法是怎么利用前缀数组来优化效果的。
例如,目标串 \(S=\)"\(bfbfkmpbfbfbfbfkmpbf\)",模式串 \(P=\)"\(bfbfbfkmpbf\)",前缀数组 \(prefix=\{-1,0,0,1,2,3,4,0,0,0,1\}\)。用 $i $表示模式串 $P $ 的当前下标,\(j\) 表示目标串 $S $的当前下标,初始值均为 0,如图1 所示。
图1 KMP算法前缀数组初始化
当 \(i<4、j<4\) 时,\(S[i]\) ==\(P[j]\);当$ i=4、j=4 \(时,\)S[i]\(不等于\)P[j]$,如图2所示。
图2 KMP算法第一次匹配
此时,\(prefix\) 数组中下标 $i $ 所对应的元素为 2,所以将字符串 \(P\) 往后移动,直至 $i $ 指向下标为 2 的字符,如图 3 所示:
图3 KMP算法第二次匹配
移动完成之后,发现 \(S[i]\) 不等于 \(P[j]\),\(i\) 在 \(prefix\) 数组中所对应的元素为 0,再将字符串 \(P\) 往后移动直到 $i $ 指向下标为 0 的字符,如图4所示。
图4 KMP算法第三次匹配
移动结束,$S[i] $ 仍然不等于\(P[j]\),并且 $ i$ 在 \(prefix\) 数组中所对应的元素为 -1,如果将 $i $ 赋值为 -1,则在数组中已经越界,所以这里将$i $和 \(j\)都加上1,如图5所示。
图5 KMP算法第四次匹配
此时 \(i=1,j=5,S[i]\) 不等于 \(P[j]\),$i \(在\)prefix$数组中所对应的元素为\(0\),因此,将字符串\(P\)往后移动,直至 \(i\) 指向 \(0\),如图5所示。
移动后,$S[i] $不等于 \(P[j]\) ,并且 \(prefix[i]\) 的值为-1,因此将 $i $ 和 $j $的值加 1,如图6所示。
图6 KMP算法第五次匹配
图7 KMP算法第六次匹配
此时\(S[i]\)不等于\(P[j]\),\(prefix[i]=0\),将字符串\(P\)往后移动,直至 $i $指向 \(0\),如图8所示。
图8 KMP算法第七次匹配
此时,$S[i] $ 不等于 \(P[j]\),\(prefix[i]\) 的值为-1,将 \(i、j\) 的值各自加1,如图9所示。
图9 KMP算法第八次匹配
此时,\(S[i]\) 依旧不等于 \(P[j]\),\(prefix[i]\) 的值为 0,因此继续将字符串 $P $往后移动,直至 $i $指向0,如图10所示。
图10 KMP算法第九次匹配
移动后,\(S[i]\) 等于 \(P[j]\),向后继续依次匹配,当\(i=6、j=13\)时,\(S[i]\)不等于\(P[j]\),如图11所示。
图11 KMP算法第十次匹配
此时,\(prefix[i]\) 的值为4,将字符串往后移动,直至i指向字符串 \(P\) 的下标值为4的字符,如图12所示。
图12 KMP算法第十一次匹配
此时,\(S[i]\) 等于$ P[j]$,继续往后匹配,均匹配成功,即在目标串中找到了一个与模式串匹配的子串,如图13所示,算法结束。
图13 KMP算法第十二次匹配
- 代码实现
# 构建前缀表
def prefix_table(pattern, prefix, n):
prefix[0] = 0
len, i = 0, 1
while i < n:
if pattern[i] == pattern[len]:
len += 1
prefix[i] = len
else:
if len > 0:
len = prefix[len - 1]
else:
prefix[i] = len
i += 1
# 移动前缀表
def move_prefix_table(prefix, n):
for i in range(n-1, 0, -1):
prefix[i] = prefix[i-1]
prefix[0] = -1
# 实现KMP搜索算法
def kmp_seRCH(text, pattern):
n, m = len(pattern), len(text)
prefix = [0 for _in range(n)]
prefix_table(pattern, prefix, n)
move_prefix_table(prefix, n)
i, j = 0, 0
while i < m:
if j == n - 1 and text[i] == parttern[j]:
print("Found pattern at {}".format(i - j))
j = prefix[j]
if text[i] == pattern[j]:
i, j = i+1, j+1
else:
j = prefix[j]
if j == -1:
i, j = i+1, j+1
# 调试
if __name__ == '__main__':
pattern = "ABABCABAA"
text = "FJKABABCABAAFDSF"
kmp_search(text, pattern)
# 运行结果
found pattern at 3
二、 广义表
2.1 定义
广义表是由 $n $ 个类型相同的数据元素 \((a_1、a_2、…、a_n)\) 组成的有限序列。
广义表的元素可以是单个元素,也可以是一个广义表。通常广义表记作:
其中,$GL $是广义表的名称, $n $是广义表的长度
2.2 基本概念
- 原子: 在广义表 \(GL\) 中, 如果 \(a_i\) 是单个元素,则称 \(a_i\) 为原子;
- 子表: 在广义表 \(GL\) 中, 如果 \(a_i\) 是一个广义表,则称 \(a_i\) 为广义表 \(GL\) 的子表;
- 表头: 在广义表 \(GL\) 中,如果 \(a_1\) 不为空,则称 \(a_1\) 为广义表的表头
- **表尾: **在广义表 $GL $中,除表头 $a_1 $外其余元素组成的表称为表尾
- 深度: 广义表 $GL $中括号嵌套的最大层数称为广义表的深度
- 长度: 广义表 $GL $中的元素个数称为广义表的长度
2.3 存储结构
广义表有两种数据元素,分别是子表和原子,因此需要两种结构的节点:
- 一种是表节点,用来表示子表,如图2-1所示。表节点由三个域组成,即标志域 \(tag\)、指向表头节点的指针域 \(ph\)、指向表尾节点的指针域 \(pt\)。表节点的标志域 \(tag=1\)。
图2-1 广义表节点
- 另一种是原子节点,用来表示原子,如图2-2所示。
原子节点由两个域组成,即标志域 \(tag\)、值域 \(atom\)。原子节点的标志域 \(tag=0\)
图2-2 广义表原子节点
这里介绍广义表的头尾链表存储结构。若广义表不空,则可分解成由表头和表尾组成。
广义表的头尾链表存储结构代码实现如下:
class Node(object):
def __init__(self, ph, pt, tag, atom):
self.ph = ph
self.pt = pt
self.tag = tag
self.atom = atom
若广义表\(A=()\),则其头尾链表存储结构如图2-3所示。
图2-3 广义表A=()
若广义表\(B=(a)\),则其头尾链表存储结构如图2-4所示。
图2-4 广义表B=(a)
若广义表\(C=((a))\),则其头尾链表存储结构如图2-5所示。
图2-5 广义表C=((a))
若广义表\(D=(a,(b,c),(d,(e,f))\),则其头尾链表存储结构如图2-6所示。
图2-6 广义表D=(a,(b,c),(d,(e,f))
2.4 基本操作
1. 求广义表的长度
广义表的长度是指广义表包含节点的个数,只需要扫描其有多少个节点即可。
代码实现如下:
def length(self):
# 判断是否有表
if self.root is None or self.root.pt is None:
return -1
tlen = 0
node = self.root
# 求长度只需要判断第一层的长度,判断到下一个表姐的为空即结束
while node.pt is not None:
node = node.pt
# 判断该表姐的是否有值
if node.ph is None and node.pt is None:
break
tLen += 1
return tLen
2. 求广义表的深度
广义表的深度是指广义表中嵌套表的最大嵌套深度,这里需要使用递归机制求解每个表节点的深度,并取出最大的嵌套深度。
代码实现如下:
def Listdepth(self, node):
# 递归遍历层数以获取深度
# 判断节点是否为原子节点,若是原子节点,则表示已到底,后面没有节点,返回0
if node is None or node.tag is 0:
return 0
depHeader = 1+ self.Listdepth(node.ph)
depTear = self.Listdepth(node.pt)
if depHeader > depTear:
return depHeader
else:
return depTear
2.5 小结
广义表是线性表的拓展,能够表示树结构和图结构(后续讨论)。广义表有两种存储结构,一种是头尾链表存储结构,另一种是拓展线性存储结构,本次只介绍了头尾链表的存储结构。
标签:字符,匹配,前缀,python,算法,实现,prefix,广义 来源: https://www.cnblogs.com/lyxLearningNotes/p/16126836.html