当前位置:   article > 正文

《数据结构与算法Python语言描述》裘宗燕 笔记 第三章 线性表_数据结构与算法裘宗燕答案

数据结构与算法裘宗燕答案

 

《数据结构与算法Python语言描述》裘宗燕 笔记系列
该系列笔记结合PPT的内容整理的,方便以后复习,有需要的朋友可以看一下。

源码重新整理了

 

地址:https://github.com/StarsAaron/DS/tree/master

要理解数据结构处理的问题,需要对计算机内存、存储管理方面重要基本问题有一些了解。

线性表是一种元素集合,其中还记录着元素间的一种顺序关系

 

作为一种包含元素(可以没有,也可以有许多)的数据结构,通常都需要提供一些“标准”操作,例如:

(1)创建和销毁这种数据结构(的实例)

(2)判断一个数据结构是否空(没有元素)。如果数据结构的容量有限制,还需判断它是否满(不能再加入新元素)

(3)向结构中加入元素或从中删除元素

(4)访问结构里的元素

 

两种实现线性表的方式:顺序表和链接表

 

顺序表的基本实现方式

(1)元素顺序存放在一片足够大的连续存储区里。表中首元素存入存储区开始的位置,其余元素依次顺序存放

(2)通过元素在存储区里的“物理位置”表示元素之间的逻辑顺序关系(隐式表示元素间的关系)

 

元素大小通常可以静态确定(如元素是整数,实数,或包含若干大小确定的元素的复杂结构)

基于各方面考虑,人们提出了两种基本实现模型

(1)将表元素顺序存放在的一大块连续的存储区里,这样实现的表也称为顺序表(或连续表),元素顺序有自然的表示

(2)将表元素存放在通过链接构造起来的一系列存储块里(链接表

 

一些操作的复杂性是常量 O(1)。现在特别考虑定位加入和删除操作的复杂性(首端加入删除是它们的特例)

在 个元素的顺序表里下标 处加入元素,需要移动 n - i 个元素;删除下标为 的元素需要移动 n - i - 1个元素

设在位置 加入和删除元素的概率分别是 pi 和 pi’

加入操作平均移动次数 

删除操作平均移动次数

考虑平均复杂性时要考虑实例分布,依赖于实际情况

如果各种情况平均分布,要维持顺序,平均时间复杂性是 O(n) 坏情况是首端加入/删除,要求维持顺序时复杂性也是 O(n)

 

访问操作

(1)不需要扫描表的操作,复杂性都是 O(1)

(2)扫描表内容操作复杂性 O(n)。如根据元素值检索,累计元素值等

 

顺序实现(顺序表)总结

优点:

(1) O(1) 时间(随机,直接)按位置存取元素

(2) 元素存储紧凑,除表元素存储外只需 O(1) 空间存放辅助信息

 

顺序表的模型包括两部分内容

(1) 一个元素存储区,存放表中的实际元素

(2) 若干单元,存放一个表的全局信息(容量,表元素个数)

 

一个数据结构应该具有一个对象的整体形态。对顺序表,就是要把这两块信息组织关联起来。表的全局信息只需常量存储,存在两种组织方式

一体式实现:

分离式实现:通过链接联系

缺点: 

(1) 需要连续的大块存储区存放表中的元素,可能有大量空闲单元 

(2) 加入删除操作时通常要移动许多元素

(3) 需要考虑元素存储区的大小(有时事先很难估计)

 

链接表:单链表,双链表

实现线性表的另一方式是基于链接结构,用链接显式地表示元素之间的顺序关联。基于链接技术实现的线性表称为链接表或链表

 

实现链接表的基本想法:把表元素分别存储在一批独立的存储块(称为结点)里

   o 保证从一个元素的结点可找到与其相关的下一个元素的结点

   o 在结点里用链接的方式显式记录元素(结点)之间的关联

 

这样,只要知道表中第一个结点,就能顺序找到表里其他元素

 

链接表有多种不同的组织方式。

 

单链表(下面简称链表)结点的形式(链接域保存下一结点的标识)

在单链表里,与表里的 n 个元素对应的 n 个结点通过链接形成一条结点链。从表里的任一个结点都可以找到保存下一个元素的结点

 

基本操作:

创建空表: O(1)

删除表:在 Python 里是 O(1)。当然存储管理也需要时间

判断空表: O(1)

 

加入元素(都需要加一个 T(分配) 的时间):

首端加入元素: O(1)

尾端加入元素: O(n),因为需要找到表的最后结点

定位加入元素: O(n),平均情况和最坏情况

删除元素:

首端删除元素: O(1);尾端删除: O(n)

定位删除元素: O(n),平均情况和最坏情况

其他删除通常需要扫描整个表或其一部分, O(n) 操作

其他操作,如果需要扫描整个表或其一部分,都是 O(n) 操作。如

求表的长度(表中元素个数)

定位表中的元素;等等

 

一类典型的表操作是扫描整个表,对表中每个元素做同样的工作(即遍历操作)。例如,输出所有的元素值,将它们累积到一个变量里等。

这种工作可以通过一个循环完成

遍历操作的复杂性应该是 O(n) * T(元素操作)

 

循环单链表

单链表只能支持首端元素加入/删除和尾端加入的 O(1) 实现,如果希望高效实现两端的加入/删除,就必须修改结点的设计

结点间双向链接不仅支持两端的高效操作,一般结点操作也更方便

 

  1. class LCList: # class of Circular Linked List
  2. def __init__(self):
  3. self.rear = None # 表对象只有一个 rear 域
  4. def isEmpty(self):
  5. return self.rear is None
  6. def prepend(self, elem): # add element in the front end
  7. p = LNode(elem, None)
  8. if self.rear is None:
  9. p.next = p # 表中第一个结点建立初始的循环链接
  10. self.rear = p
  11. else:
  12. p.next = self.rear.next # 链在尾结点之后,就是新的首结点
  13. self.rear.next = p
  14. def append(self, elem): # add element in the rear end
  15. self.prepend(elem) # 直接调用前段加入操作
  16. self.rear = self.rear.next # 修改 rear 使之指向新的尾结点
  17. def pop(self): # pop out head element
  18. if self.rear is None:
  19. raise ValueError
  20. p = self.rear.next
  21. if self.rear is p: # rear 等于其 next,说明表中只有一个结点
  22. self.rear = None # 弹出唯一结点后 rear 置空
  23. else:
  24. self.rear.next = p.next #一般情况,删去一个结点
  25. return p.elem
  26. # 简单输出所有元素的方法
  27. def printall(self):
  28. p = self.rear.next
  29. while True:
  30. print(p.elem)
  31. if p is self.rear:
  32. break
  33. p = p.next

 

支持首尾端高效操作的双向链接表(双链表)结构:

从任一结点出发,可以直接找到前后的相邻结点。而对单链表,找后面一个结点很方便,而找前一结点就必须从表头开始逐一检查

每个结点需要增加了一个前向引用域,指向前一结点(存储开销),与前面一样,增加了尾结点引用,才能实现高效的尾端操作

 

  1. # 定义一个新结点类
  2. class LDNode(LNode): # class of Double Linked Nodes
  3. def __init__(self, prev, elem, nxt):
  4. LNode.__init__(self, elem, nxt)
  5. self.prev = prev # 扩充了一个前一结点指针
  6. # 双链表
  7. class LDList(LList1): # class of Double Linked List
  8. def __init__(self):
  9. LList1.__init__(self) # 基于带尾结点引用的单链表派生一个双链表类
  10. # 加入元素的一对操作
  11. def prepend(self, elem):
  12. p = LDNode(None, elem, self.head)
  13. self.head = p
  14. if self.rear is None: # insert in empty list
  15. self.rear = p
  16. else: # otherwise, create the prev reference
  17. p.next.prev = p
  18. # 加入元素的一对操作
  19. def append(self, elem): # 与 prepend 对称
  20. p = LDNode(self.rear, elem, None)
  21. self.rear = p
  22. if self.head is None: # insert in empty list
  23. self.head = p
  24. else: # otherwise, create the next reference
  25. p.prev.next = p
  26. # 弹出(删除)元素
  27. def pop(self):
  28. if self.head is None:
  29. raise ValueError
  30. e = self.head.elem
  31. self.head = self.head.next # 删除当前首结点
  32. if self.head is None:
  33. self.rear = None # 如果删除后表空,把 rear 也置空
  34. else:
  35. self.head.prev = None # 首结点的 prev 链接置空
  36. return e
  37. # 与 pop 对称
  38. def poplast(self):
  39. if self.head is None:
  40. raise ValueError
  41. e = self.rear.elem
  42. self.rear = self.rear.prev
  43. if self.rear is None:
  44. self.head = None # 删除后表空,把 head 也置空
  45. else:
  46. self.rear.next = None
  47. return e

 

循环双链表

 

让表尾节点的next域指向表首节点,让表首节点的prev域指向尾节点。

首尾两端的元素加入/删除操作O(1)复杂度

链表元素反转

反转一个表顺序的元素,算法通常用两个下标,逐对交换元素位置

- 在双链表上很容易采用该方法实现表元素反转,留作个人练习

- 在单链表上也可以实现,但单链表不支持从后向前找结点,只能每次从头开始找,算法需要 O(n2) 时间。自己想想有别的办法吗?

 

注意:对顺序表,修改表元素顺序的方法只有一种:在表中搬动元素;而对链表却有两种方法:在结点之间搬动元素,或修改链接关系

- 在单链表里通过搬元素方式实现元素反转,不方便,且效率低

- 下面考虑基于修改链接的方法,看看有什么可能性

 

首先:表前端加入和删除元素或结点是最方便的操作, O(1)

- 如果不断在前端加入结点,最早放进去的结点在最后(是尾结点)

- 从表的前端取下结点,最后取下的是尾结点

- 结合这两个过程,很容易做出一个高效的反转算法

 

  1. def rev(self):
  2. p = None
  3. while self.head is not None:
  4. q = self.head
  5. self.head = q.next # 摘先原来的首结点
  6. q.next = p
  7. p = q # 将刚摘下的结点加入 p 引用的结点序列
  8. self.head = p # 反转后的结点序列已经做好,重置表头链接

 

链表元素排序

把一组物品按顺序排列是在真实世界里经常需要做的工作

- 数据处理中也经常需要将数据排序

- 如 lst 的值是 list 对象, lst.sort() 完成 lst 对象中元素的排序工作;

sorted(lst) 生成一个新表,其元素是 lst 元素的排序结果

 

用链接表存储数据,也常需要考虑其中元素的排序问题

- 排序是重要的数据处理问题,人们提出了许多算法(后面考虑)

- 下面考虑一种简单的排序算法:插入排序

 

插入排序的基本想法:

- 操作中维护一个排好序的片段,初始时该段只包含一个元素

- 每次从未排序部分取出一个元素,加入已排序片段中正确位置,保持该片段的正确排序状态;所有元素都加入排序片段时工作完成

- 通常在表的前部积累排好序的片段

 

一个对 list 的元素排序的函数

 

  1. def listSort(lst):
  2. for i in range(1, len(lst)): # seg [0:0] is sorted
  3. x = lst[i]
  4. j = i
  5. while j > 0 and lst[j - 1] > x: # moving one by one
  6. lst[j] = lst[j - 1] # in reversed-order
  7. j -= 1
  8. lst[j] = x

 

 

单链表排序

下面考虑单链表元素的排序

- 注意:只有 next 链接,不能从后向前找结点(找元素)

- 存在完成排序的两种可能做法:移动元素,或调整链接

- 下面分别考虑采用这两种技术实现的算法

 

先考虑基于移动元素的单链表排序

注意,为有效实现,算法中只能按从头到尾的方向检查和处理,做法仍然是每次拿出一个元素,在已排序序列中找到正确位置插入

 

  1. def sortm(self):
  2. if self.head is None:
  3. return
  4. crt = self.head.next # 从首结点之后开始处理
  5. while crt is not None:
  6. x, p = crt.elem, self.head
  7. while p is not crt and p.elem <= x: # 跳过小元素
  8. p = p.next
  9. while p is not crt: # 倒换大元素,完成元素插入的工作
  10. y = p.elem
  11. p.elem = x
  12. x = y
  13. p = p.next
  14. crt.elem = x # 最后的回填
  15. crt = crt.next

 

 

通过调整链接关系完成排序工作

基本的处理模式与前一算法类似,但这里不在结点之间移动表元素,而是把被处理结点取下来接到正确位置,逐步完成排序工作

 

  1. # 通过调整链接关系完成排序
  2. def sort(self):
  3. if self.head is None:
  4. return
  5. last = self.head # 初始,排序段只有一个结点
  6. crt = last.next
  7. while crt is not None: # 循环,一次处理一个结点
  8. p = self.head
  9. q = None # 设置扫描指针初值
  10. while p is not crt and p.elem <= crt.elem:
  11. q = p
  12. p = p.next # 顺序更新两个扫描指针
  13. if p is crt: # p 是 crt 时不用修改链接,设置 last 到下一结点 crt
  14. last = crt
  15. else:
  16. last.next = crt.next # 取下当前结点
  17. crt.next = p # 接好后链接
  18. if q is None:
  19. self.head = crt # 作为新的首结点
  20. else:
  21. q.next = crt # 或者接在表中间
  22. crt = last.next # 无论什么情况, crt 总是 last 的下一结点
  23. # end of class LList

 

实例: Josephus问题

问题:设有 n 个人围坐一圈,现在从第 k 个人开始报数,报到第 m 的人退出。然后继续报数,直至所有人退出。输出出列人顺序编号

 

下面考察基于几种想法和数据结构的解决方法

 

方法1:Josephus问题:数组算法

基于 Python list 和固定大小的“数组”

- 初始:建立一个包含 n 个人(的编号)的 list

- 找到第 k 个人,从那里开始,处理过程中,通过把相应表元素修改为 0 表示人已不在位

- 反复做:

    o 数 m 个(尚在坐的)人

    o 把表示第 m 个人的元素修改为 0

    注意:数到 list 最后元素之后转到下标为 0 的元素继续

 

  1. def JosephusA(n, k, m):
  2. people = list(range(1, n + 1))
  3. num, i = 0, k - 1
  4. for num in range(n):
  5. count = 0
  6. while count < m:
  7. if people[i] > 0:
  8. count += 1
  9. if count == m:
  10. print(people[i], end="")
  11. people[i] = 0
  12. i = (i + 1) % n
  13. if num < n - 1:
  14. print(", ", end="")
  15. else:
  16. print("")
  17. return

 

这里的主要麻烦是表下标的计数和元素计数脱节

复杂度不容易分析,内循环的总迭代次数是i加1,与m和n有关,m影响找到下一个元素得到操作步数。

 

方法2:Josephus问题:连续表算法

现在考虑另一算法:将保存人员编号的 list 按照表的方式处理

-  算出应该退出的人之后,将其从表里删除

-  计算过程中表越来越短,用 num 表示表的长度,每退出一人,表长度 num 减一

-  至表长度为 0 时工作结束

 

由于表里全是有效元素

-  元素计数与下标计数统一了

-  下标更新可以用 i = (i + m - 1) % num 统一描述

 

  1. def JosephusL(n, k, m):
  2. people = list(range(1, n + 1))
  3. if k < 1 or k > n:
  4. raise ValueError
  5. num, i = n, k - 1
  6. for num in range(n, 0, -1):
  7. i = (i + m - 1) % num
  8. print(people.pop(i), end="")
  9. if num > 1:
  10. print(", ", end="")
  11. else:
  12. print("")
  13. return

 

貌似线性时间算法,但不是

- pop 操作需要线性时间 O(n)

- 算法复杂性是 O(n^2)

 

方法3:Josephus问题:循环链表算法

考虑基于循环单链表实现一个算法

- 从形式看,循环单链表很好地表现了围坐一圈的人

- 顺序地数人头,可以自然地反映为在循环表中沿 next 链扫描

- 一个人退出,可以用删除相应结点的操作模拟。这样做之后,就可以沿着原方向继续数人头

 

算法应分为两个阶段:

- 建立包含指定个数(和内容)的结点的循环单链表,可以通过从空表出发逐个在尾部加入元素的方式完成

- 循环计数,并删去需要退出的结点

 

具体实现可以有多种方式,例如:为原循环单链表增加一个循环数数的函数,然后写一段程序建立所需的循环单链表,并完成操作

 

下面实现基于循环单链表类,派生出一个专门的类,用其初始化方法完成全部工作

 

  1. class Josephus(LCList):
  2. def turn(self, m):
  3. for i in range(m):
  4. self.rear = self.rear.next
  5. def __init__(self, n, k, m):
  6. LCList.__init__(self)
  7. for i in range(n):
  8. self.append(i + 1)
  9. self.turn(k - 1)
  10. while not self.isEmpty():
  11. self.turn(m - 1)
  12. print(self.pop(), end="")
  13. if not self.isEmpty():
  14. print(", ", end="")
  15. else:
  16. print("")

 

这里采用另一种实现方法

- 从 LCList 派生出一个类

- 用这个类的初始化函数完成所有工作

 

派生类 Josephus 增加了新方法 turn,它将循环表对象的 rear 指针沿 next 移 m 步

初始化函数调用基类 LCList的初始化函数建立一个空表

第一个循环建立包含 n 个结点的循环表,复杂度为O(n)

最后的循环逐个弹出结点并输出结点里保存的编号,移动m位置复杂度为O(1)

 

算法复杂性 O(m*n)

 

链接表:优点和缺点

优点:

- 表结构是通过一些链接起来的结点形成的,结构很容易调整修改

- 不修改结点里的数据元素,只通过修改链接,就能灵活地修改表的结构和内容。如加入/删除一个或多个元素,翻转整个表,重排元素的顺序,将一个表分解为两个或多个等

- 整个表由一些小存储块构成,较容易安排和管理(不是我们的事)

 

缺点,一些操作的开销大:

- 基于位置找到表中元素需要线性时间

- 尾端操作需要线性时间(增加尾指针可以将尾端加入元素变为常量操作,但仍不能有效实现尾端删除)

- 找当前元素的前一元素需要从头扫描表元素(双链表可以解决这个问题,但每个结点要付出更多存储代价),应尽量避免

 

每个元素增加了一个链接域(存储代价),双链表增加两个链接域

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号