python实现数据结构--串和广义表

字符串和广义表

一、 字符串

1.1 定义

串也称为字符串,是由零个或者多个字符组成的有限序列。串仅由字符组成,记作:

S S S=“ a 1 a 2 . . . a n a_1a_2...a_n a1a2...an

其中, S S S 是串名,双引号括起来的字符序列 a 1 a 2 … a n a_1a_2…a_n a1a2an 是串值, n 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算法。

  1. 算法思想

从目标串 S = S= S= s 0 s 1 … s n − 1 s_0s_1…s_{n-1} s0s1sn1” 的第一个字符开始,与模式串 P = = P== P== p 0 p 1 … p m − 1 p_0p_1…p_{m-1} p0p1pm1” 的第一个字符进行比较:

  • 若相等,则继续逐个比较后续字符

  • 若不相等,则从目标串的下一个字符起重新和模式串的字符进行比较

    以此类推,直至模式串 P P P 中的每个字符依次和目标串 S S S 中的一个连续的字符序列的相应字符都相等,则称匹配成功,返回和模式串P中第一个字符相等的字符在目标串S中的序号;否则说明模式串P不是目标串的子串,匹配不成功,返回-1。

  1. 算法分析

例如,目标串 S = S= S= a b c d e a b c d f abcdeabcdf abcdeabcdf”,模式串 P = P= P= a b c d f abcdf abcdf”,判断模式串 P P P 与目标串 $ S $ 是否匹配,根据Brute-Force算法的思想分析匹配的过程如下。

​ 假设 $ i $ 为目标串 S S S 的当前下标索引,$j $为模式串 $ P$ 的当前下标索引,默认 i 、 j i、j ij 的初始值为 0 0 0

​ 第一次匹配,从 i = 0 、 j = 0 i=0、j=0 i=0j=0 开始匹配,当 $j=4、i= 4 时 , 匹 配 失 败 。 因 此 , 要 将 4 时,匹配失败。因此,要将 4 i $回溯到 i = 1 , j = 0 i=1,j=0 i=1j=0,如图所示:

image-20220409153122775

第二次匹配,从 i = 1 、 j = 0 i=1、j=0 i=1j=0开始匹配,不难发现此时匹配失败,如图所示。因此,要修改$ i、j $的值,重新开始匹配,从 i = 2 、 j = 0 i=2、j=0 i=2j=0 开始。

image-20220409153459391

第三次匹配, S [ i = 2 ] ! = P [ j = 0 ] S[i=2]!=P[j=0] S[i=2]!=P[j=0],如下图所示。第三次匹配结束,修改 $i $ 的值, i = 3 i=3 i=3

image-20220409153645000

第四次匹配, S [ i = 3 ] ! = P [ j = 0 ] S[i=3]!=P[j=0] S[i=3]!=P[j=0],如下图所示。第四次匹配结束,修改 $i $ 的值, i = 4 i=4 i=4

image-20220409153846944

​ 第五次匹配, S [ i = 4 ] ! = P [ j = 0 ] S[i=4]!=P[j=0] S[i=4]!=P[j=0],如下图所示。第五次匹配结束,修改 i i i 的值, i = 5 i=5 i=5:

image-20220409154013478

第六次匹配,从$ i=5、j=0 $开始匹配,当 $i=9、j=4 $时匹配成功,如下图所示:

image-20220409154149890

从上面的分析可以得到,若 m m m 为目标串长度,$n $为模式串长度,则 Brute-Force 算法在匹配时所花费的时间分为以下两种情况来分析。:

  • 最好的情况下,第一次就匹配成功,目标串与模式串匹配,比较次数为模式串的长度 $ n$,时间复杂度为 O ( n ) O(n) O(n)

  • 最坏情况下,每次匹配比较至模式串的最后一个字符又失败,并且比较了目标串中所有长度为 $n $ 的子串,时间复杂度为 O ( n × ( n − m + 1 ) = O ( n × m ) O(n×(n-m+1)=O(n×m) On×nm+1=On×m

  1. 代码实现
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算法
  1. 算法思想

从目标串 S S S 的第一个字符开始扫描,逐一与模式串 $P $ 对应的字符进行匹配:

  • 若该组字符匹配,则继续匹配下一组字符;

  • 若该组字符不匹配,则并不是简单地从目标串下一个字符开始新一轮的匹配,而是通过一个前缀数组跳过不必要匹配的目标串字符,以达到优化效果。

    从KMP的算法思想中可以得到两个信息:

    ​ 一是前缀数组是什么以及怎么构建前缀数组,

    ​ 二是在得到前缀数组后怎么利用它达到优化的效果。

  1. 前缀数组

​ 前缀、后缀的概念:

  • 前缀:前缀就是除了最后一个字符以外,一个字符串的全部头部集合。例如,有一个字符串 S = S= S= a b c abc abc”,其前缀为{“ a a a”,“ a b ab ab”};
  • 后缀:后缀就是指除了第一个字符以外,一个字符串的全部尾部集合。例如,有一个字符串 S = S= S= a b c abc abc”,其后缀为{“ c c c”,“ b c bc bc”}。

在这里要注意,从中间位置截取的一段字符串是不能被称为前缀或后缀的。例如,字符串 S = S= S= a b c d abcd abcd”,字符串 “ b c bc bc” 不属于前缀数组或者后缀数组。

下面通过一个例子来讲解如何构建前缀数组。现在有一个字符串S=“ b f b f b f k m p b f bfbfbfkmpbf bfbfbfkmpbf”。

​ 字符串 “ b b b” 的前缀和后缀都为空集,最长共有元素长度为0。

​ 字符串" b f bf bf"的前缀为{“ b b b”},后缀为{“ f f f”},没有相同的前缀子串和后缀子串,最长共有元素长度为0。

​ 字符串" b f b bfb bfb"的前缀为{“ b b b”,“ b f bf bf”},后缀为{“ b b b”,“ f b fb fb”},相同的前缀子串和后缀子串为" b b b",最长共有元素长度为1。

字符串" b f b f bfbf bfbf"的前缀为{“ b b b”,“ b f bf bf”,“ b f b bfb bfb”},后缀为{“ f f f”,“ b f bf bf”,“ f b f fbf fbf”},相同的前缀子串和后缀子串为" b f bf bf",最长共有元素长度为2。

​ 字符串" b f b f b bfbfb bfbfb"的前缀为{“ b b b”,“ b f bf bf”,“ b f b bfb bfb”,“ b f b f bfbf bfbf”},后缀为{“ b b b”,“ f b fb fb”,“ b f b bfb bfb”,“ f b f b fbfb fbfb”},相同的前缀子串和后缀子串为" b f b bfb bfb",最长共有元素长度为3。

​ 以此类推…

基于上述分析,可以获得一个前缀数组 p r e f i x = { 0 , 0 , 1 , 2 , 3 , 4 , 0 , 0 , 0 , 1 , 2 } prefix=\{0,0,1,2,3,4,0,0,0,1,2\} prefix={00123400012}。为了方便后面应用KMP算法进行计算,将前缀数组的第一个位置的元素置为 -1,将当前前缀数组的元素都往后移动一个位置,将最后一个位置的元素删除,得到一个新的前缀数组, p r e f i x = { − 1 , 0 , 0 , 1 , 2 , 3 , 4 , 0 , 0 , 0 , 1 } prefix=\{-1,0,0,1,2,3,4,0,0,0,1\} prefix={10012340001}

  1. 算法分析

由上可知如何构建一个前缀数组prefix,现在来分析KMP算法是怎么利用前缀数组来优化效果的。

​ 例如,目标串 S = S= S= b f b f k m p b f b f b f b f k m p b f bfbfkmpbfbfbfbfkmpbf bfbfkmpbfbfbfbfkmpbf”,模式串 P = P= P= b f b f b f k m p b f bfbfbfkmpbf bfbfbfkmpbf”,前缀数组 p r e f i x = { − 1 , 0 , 0 , 1 , 2 , 3 , 4 , 0 , 0 , 0 , 1 } prefix=\{-1,0,0,1,2,3,4,0,0,0,1\} prefix={10012340001}。用 $i $表示模式串 $P $ 的当前下标, j j j 表示目标串 $S $的当前下标,初始值均为 0,如图1 所示。

image-20220409161953302
图1.KMP算法前缀数组初始化

i < 4 、 j < 4 i<4、j<4 i<4j<4 时, S [ i ] S[i] S[i] == P [ j ] P[j] P[j];当$ i=4、j=4 时 , 时, S[i] 不 等 于 不等于 P[j]$,如图2所示。

image-20220409162638908
图2 KMP算法第一次匹配

此时, p r e f i x prefix prefix 数组中下标 $i $ 所对应的元素为 2,所以将字符串 P P P 往后移动,直至 $i $ 指向下标为 2 的字符,如图 3 所示:

image-20220409162932263
图3 KMP算法第二次匹配

移动完成之后,发现 S [ i ] S[i] S[i] 不等于 P [ j ] P[j] P[j] i i i p r e f i x prefix prefix 数组中所对应的元素为 0,再将字符串 P P P 往后移动直到 $i $ 指向下标为 0 的字符,如图4所示。

image-20220409163149915
图4 KMP算法第三次匹配

移动结束,$S[i] $ 仍然不等于 P [ j ] P[j] P[j],并且 $ i$ 在 p r e f i x prefix prefix 数组中所对应的元素为 -1,如果将 $i $ 赋值为 -1,则在数组中已经越界,所以这里将$i $和 j j j都加上1,如图5所示。

image-20220410165404065
图5 KMP算法第四次匹配

此时 i = 1 , j = 5 , S [ i ] i=1,j=5,S[i] i=1j=5S[i] 不等于 P [ j ] P[j] P[j],$i 在 在 prefix$数组中所对应的元素为 0 0 0,因此,将字符串 P P P往后移动,直至 i i i 指向 0 0 0,如图5所示。

​ 移动后,$S[i] $不等于 P [ j ] P[j] P[j] ,并且 p r e f i x [ i ] prefix[i] prefix[i] 的值为-1,因此将 $i $ 和 $j $的值加 1,如图6所示。

image-20220410165737315
图6 KMP算法第五次匹配
image-20220410165919196
图7 KMP算法第六次匹配

此时 S [ i ] S[i] S[i]不等于 P [ j ] P[j] P[j] p r e f i x [ i ] = 0 prefix[i]=0 prefix[i]=0,将字符串 P P P往后移动,直至 $i $指向 0 0 0,如图8所示。

image-20220410170203919
图8 KMP算法第七次匹配

此时,$S[i] $ 不等于 P [ j ] P[j] P[j] p r e f i x [ i ] prefix[i] prefix[i] 的值为-1,将 i 、 j i、j ij 的值各自加1,如图9所示。

image-20220410170807327
图9 KMP算法第八次匹配

​ 此时, S [ i ] S[i] S[i] 依旧不等于 P [ j ] P[j] P[j] p r e f i x [ i ] prefix[i] prefix[i] 的值为 0,因此继续将字符串 $P $往后移动,直至 $i $指向0,如图10所示。

image-20220410170953477
图10 KMP算法第九次匹配

​  移动后, S [ i ] S[i] S[i] 等于 P [ j ] P[j] P[j],向后继续依次匹配,当 i = 6 、 j = 13 i=6、j=13 i=6j=13时, S [ i ] S[i] S[i]不等于 P [ j ] P[j] P[j],如图11所示。

image-20220410171122173
图11 KMP算法第十次匹配

​ 此时, p r e f i x [ i ] prefix[i] prefix[i] 的值为4,将字符串往后移动,直至i指向字符串 P P P 的下标值为4的字符,如图12所示。

image-20220410171311212
图12 KMP算法第十一次匹配

此时, S [ i ] S[i] S[i] 等于$ P[j]$,继续往后匹配,均匹配成功,即在目标串中找到了一个与模式串匹配的子串,如图13所示,算法结束。

image-20220410171717708
图13 KMP算法第十二次匹配
  1. 代码实现
# 构建前缀表
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 ) (a_1、a_2、…、a_n) (a1a2an) 组成的有限序列。

​  广义表的元素可以是单个元素,也可以是一个广义表。通常广义表记作:
  
G L = ( a 1 , a 2 , … , a n ) GL=(a1,a2,…,an) GL=(a1,a2,,an)
  其中,$GL $是广义表的名称, $n $是广义表的长度

2.2 基本概念

  • 原子: 在广义表 G L GL GL 中, 如果 a i a_i ai 是单个元素,则称 a i a_i ai 为原子;
  • 子表: 在广义表 G L GL GL 中, 如果 a i a_i ai 是一个广义表,则称 a i a_i ai 为广义表 G L GL GL 的子表;
  • 表头: 在广义表 G L GL GL 中,如果 a 1 a_1 a1 不为空,则称 a 1 a_1 a1 为广义表的表头
  • **表尾: **在广义表 $GL $中,除表头 $a_1 $外其余元素组成的表称为表尾
  • 深度: 广义表 $GL $中括号嵌套的最大层数称为广义表的深度
  • 长度: 广义表 $GL $中的元素个数称为广义表的长度

2.3 存储结构

广义表有两种数据元素,分别是子表和原子,因此需要两种结构的节点:

  • 一种是表节点,用来表示子表,如图2-1所示。表节点由三个域组成,即标志域 t a g tag tag、指向表头节点的指针域 p h ph ph、指向表尾节点的指针域 p t pt pt。表节点的标志域 t a g = 1 tag=1 tag=1
image-20220410171938267
图2-1 广义表节点
  • 另一种是原子节点,用来表示原子,如图2-2所示。
    原子节点由两个域组成,即标志域 t a g tag tag、值域 a t o m atom atom。原子节点的标志域 t a g = 0 tag=0 tag=0
image-20220410172107558
图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 = ( ) A=() A=(),则其头尾链表存储结构如图2-3所示。

image-20220410172224281
图2-3 广义表A=()

​ 若广义表 B = ( a ) B=(a) B=(a),则其头尾链表存储结构如图2-4所示。

image-20220410172634686
图2-4 广义表B=(a)

​ 若广义表 C = ( ( a ) ) C=((a)) C=((a)),则其头尾链表存储结构如图2-5所示。

image-20220410172820791
图2-5 广义表C=((a))

若广义表 D = ( a , ( b , c ) , ( d , ( e , f ) ) D=(a,(b,c),(d,(e,f)) D=(a,(b,c),(d,(e,f)),则其头尾链表存储结构如图2-6所示。

image-20220410173053236
图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 小结

​ 广义表是线性表的拓展,能够表示树结构和图结构(后续讨论)。广义表有两种存储结构,一种是头尾链表存储结构,另一种是拓展线性存储结构,本次只介绍了头尾链表的存储结构。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Python可以用来实现各种数据结构Python中内置的数据结构包括数字(Number)、字符(String)、列(List)、元组(Tuple)、字典(Dictionary)和集合(Set)。这些数据结构可以方便地用来存储和操作数据。除了内置的数据结构外,还可以通过导入相应的工具包来实现更复杂的数据结构,比如数组和矩阵等。在Python中,数据结构实现通常使用类来完成,通过定义类的属性和方法来描述数据结构的特征和操作。通过使用Python的强大的面向对象编程特性,可以方便地创建和使用各种数据结构。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [python 数据结构篇](https://blog.csdn.net/qq_62047219/article/details/125618390)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [Python中的数据结构](https://blog.csdn.net/qq_46344220/article/details/120514805)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值