数据结构与算法分析课下实验练习,现记录一下解答过程,欢迎大家批评指正。
声明:本题目来源于西安交通大学电信学院原盛老师,任何单位或个人在使用、转载或引用本题目时,请务必标明出处为“西安交通大学电信学院原盛老师”。
练习二:线性表基本训练
线性表是其他复杂关系数据结构的基础,尤其是对掌握链式存储技术更功不可没。
(1) 参照教材中的 ListADT 的定义,分别实现顺序线性表、带头节点的单向链表的线性表和带头节点的单向循环链表的线性表。
(2) 设置一组测试,验证 (1) 中实现的三个数据结构都可以正确完成 ADT 中所规定的逻辑。
(3) 设计一个表格,列出三个不同数据结构在各个行为上的时间复杂度。
(4) 用线性表解决约瑟夫环问题 这个问题源自约瑟夫斯·弗拉维乌斯,一位公元 1 世纪的罗马历史学家。故事略显血腥。41 名叛军被困在一个山洞里,被敌军包围。他们决定集体自杀:他们围成一圈,按顺序每隔一人处决一人,如此循环往复,直到只剩下最后一名叛军——据称他会自行了断(但这希望渺茫)。谁会是那位幸存者呢?
数学上,我们可以将问题简化为:有 n 个人,从第 1 个人开始编号到 n,每个人按照 1 到 m 的顺序报数,每数到 m 的人会被淘汰,问最后留下的人的编号是多少?解决约瑟夫环问题有多种算法,其中包括递推公式法、递归法以及利用循环链表等数据结构的解法。
请同学们使用任务 2 中的基于头结点的单循环链表的线性表编写解决约瑟夫环问题的算法。该算法的输入为人数 n 和间隔数 m,算法的输出为剩下的唯一编号。
请绘制一个图(横坐标为人数 n,纵坐标为编号),在间隔为 2 的情况下,绘制不同人数下对应的留下的唯一编号是什么?(0 < n ≤ 200)根据图形,找一找规律。
请给出这个算法的时间复杂度。
第一问
三种数据类型的定义。
顺序线性表:
#顺序线性表
class AList:def __init__(self,size):'''Alist有4个数据成员,包括存储线性表的数组listArray,表的最大长度msize,当前实际长度numInList和当前元素在数组中的位置curr'''self._listArray=[None]*sizeself._msize=sizeself._numInList=0self._curr=0#接下来添加一系列方法#首先添加清空方法def clear(self):self._numInList=0self._curr=0#添加插入方法,这里需要额外的参数:插入的数值def insert(self,item):#首先判断表是否已满if self._msize==self._numInList:return print('当前表已满,无法再插入元素')for i in range(self._numInList,self._curr,-1):self._listArray[i]=self._listArray[i-1]self._listArray[self._curr]=itemself._numInList+=1#添加append方法def append(self,item):#同样,首先判断表是否已满if self._msize==self._numInList:return print('当前表已满,无法再追加元素')self._listArray[self._numInList]=itemself._numInList+=1#判断表是否为空def isEmpty(self):if self._numInList==0:return Trueelse: return False#判断curr所指元素是否存在当前表中def isInlist(self):if self._curr<=self._numInList-1 and self._curr>=0:return Trueelse: return False#添加remove方法def remove(self):if self.isEmpty():return print('当前表为空,无法删除元素')#判断curr值是否合法if not self.isInlist():return print('当前curr值不合法')it=self._listArray[self._curr]for i in range(self._curr,self._numInList-1):self._listArray[i]=self._listArray[i+1]self._numInList-=1return it#添加setFirst方法def setFirst(self):self._curr=0#添加next方法def next(self):if self._curr<self._msize-1:self._curr+=1else:return print('当前指针在尾部,无法向后移动')#添加prev方法def prev(self):if self._curr>0:self._curr-=1else:return print('当前指针在头部,无法向前移动')#添加length方法def length(self):return self._numInList#添加setPos方法def setPos(self,pos):self._curr=pos#添加setValue方法def setValue(self,item):if self.isInlist():self._listArray[self._curr]=itemelse:return print('当前指针位置不在表内,无法重置元素')#添加currValue方法def currValue(self):if self.isInlist():return self._listArray[self._curr]else: return print('当前指针位置不在列表内,无法返回')#添加print方法def print(self):if self.isEmpty():return print('这个数组是空的')else:print('[',end=' ')for i in range(0,self._numInList):print(self._listArray[i],end=' ')print(']')
带头节点的单向链表:
import random
#创建一个创建节点的类
class Node:def __init__(self,data=None,next=None):self.data=dataself.next=next#创建带有哑结点的单链表
class Llist:def __init__(self):'''带有哑结点的单链表有三个 数据成员:分别为head头结点,tail尾节点,和curr指针'''#先创建哑结点dum=Node()self.head=dumself.tail=self.headself.curr=dum#创建一个clear方法def clear(self):self.tail=self.head#setFirst方法def setFirst(self):self.curr=self.headreturn self.curr#next方法def next(self):#首先判断此时它是否是尾节点if self.curr==self.tail:print('此时节点已指向尾节点,无法next')else:self.curr=self.curr.nextreturn self.curr#创建insert方法 插入实际上是向后插入一个元素def insert(self,item):#先创建一个节点用于放置插入元素newnode=Node()newnode.data=item#判断curr是否是在尾节点上,如果是的话要移动尾节点if self.curr==self.tail:newnode.next=self.curr.nextself.curr.next=newnodeself.tail=newnodeelse:newnode.next=self.curr.nextself.curr.next=newnode#创建删除操作 curr指向的元素并非要删除的元素,删除的是curr后边的元素def remove(self):if self.curr==self.tail:return print('当前元素不能删除')temp=self.curr.next.nextself.curr.next=Noneself.curr.next=tempif self.curr.next ==None:self.tail=self.curr#创建append方法def append(self,item):self.curr=self.tailself.insert(item)#创建prev方法def prev(self):temp=self.currself.setFirst()while(self.curr.next!=temp):self.curr=self.curr.nextreturn self.curr#创建length方法def length(self):'''一般而言,如果链表带有哑结点,那么length是不包括哑结点这个节点的'''i=0self.curr=self.headwhile(self.curr!=self.tail):i+=1self.next()return i#创建setValue方法def setVaule(self,item):self.curr.data=itemdef currValue(self):return self.curr.datadef isEmpty(self):return self.head==self.taildef isInlist(self,item):self.setFirst()while(self.curr!=self.tail):if self.curr.data==item:return Trueself.curr=self.curr.nextdef print(self):self.setFirst()self.next()while(self.curr!=self.tail):print(self.curr.data,end=' ')self.next()print(self.curr.data)
单向循环链表:单向循环链表和普通的单向链表相差不大。只需在创建时,让哑结点指向自己即可。同时,在一些方法上可能有些修改。其他相差不大。
#单向循环列表我们在课堂上并没有学过,但是其和不循环的单向列表相差不大
#同样,创建一个创建节点的类
class Node:def __init__(self,data=None,next=None):self.data=dataself.next=next#创建带有哑结点的单链表
class Llist1:def __init__(self):#先创建哑结点dum=Node()#因为是循环的,所以我们让哑结点指向自己dum.next=dumself.head=dumself.tail=dumself.curr=dum#创建一个clear方法def clear(self):self.tail=self.head#setFirst方法def setFirst(self):self.curr=self.headreturn self.curr#next方法def next(self):self.curr=self.curr.nextreturn self.curr#创建insert方法 插入实际上是向后插入一个元素def insert(self,item):#先创建一个节点用于放置插入元素newnode=Node()newnode.data=item#判断curr是否是在尾节点上,如果是的话要移动尾节点 newnode.next=self.curr.nextself.curr.next=newnodeif self.curr==self.tail:self.tail=newnode#创建删除操作 curr指向的元素并非要删除的元素,删除的是curr后边的元素def remove(self):if self.curr==self.tail:return print('当前元素不能删除')temp=self.curr.next.nextself.curr.next=Noneself.curr.next=tempif self.curr.next ==self.head:self.tail=self.curr#创建append方法def append(self,item):self.curr=self.tailself.insert(item)#创建prev方法def prev(self):temp=self.currself.setFirst()while(self.curr.next!=temp):self.curr=self.curr.nextreturn self.curr#创建length方法def length(self):'''一般而言,如果链表带有哑结点,那么length是不包括哑结点这个节点的'''#调用length后要让curr指针指向原来的位置temp=self.curri=0self.curr=self.headwhile(self.curr!=self.tail):i+=1self.next()self.curr=tempreturn i#创建setValue方法def setVaule(self,item):self.curr.data=itemdef currValue(self):return self.curr.datadef isEmpty(self):return self.head==self.taildef isInlist(self,item):self.setFirst()while(self.curr!=self.tail):if self.curr.data==item:return Trueself.curr=self.curr.nextdef print(self):self.setFirst()self.next()while(self.curr!=self.tail):print(self.curr.data,end=' ')self.next()print(self.curr.data)
第二问
测试创建的结构类型是否符合逻辑。
from 单链表 import Llist
from 单向循环链表 import Llist1
from 顺序表 import AList
import random
#测试这三种数据结构的逻辑
#从增删改查几方面
#数组实现的顺序表array=AList(10)
for i in range(5):array.append(i)
print(f'当前指针所指的数据是:{array.currValue()}')#顺序表的append并不改变curr指针的位置
print('看看数组里有哪些成员:',end='')
array.print()
print(f'这个数组的长度是:{array.length()}')
array.setFirst()
array.setValue(8)
print('修改后的数组为',end=':')
array.print()
print(f'这个元素为空吗? {array.isEmpty()}')
print('现在我插入88,看看插入后的结果:',end='')
array.insert(88)
array.print()
print('我现在设置curr指向array[5],首先看一下curr有没有指向有效元素',end=',')
array.setPos(5)
print(array.isInlist())
print('然后删除这个位置上的元素,再插入88,看看结果是',end=':')
array.remove()
array.insert(88)
array.print()
print('现在我要清空了,看看结果:',end='')
array.clear()
array.print()
print('\n\n')
# 单向链表
li=Llist()
for i in range(10):li.append(i)
print('看看链表里有哪些成员:',end='')
li.print()#print方法后curr指针就指向了尾节点
print(f'当前链表所指向的元素是:{li.currValue()}')
print(f'链表的长度是:{li.length()}')
li.setFirst()
li.next()
li.next()
li.next()
li.remove()
print(f'看看删除之后的链表:',end='')
li.print()
print('\n\n')
#单向循环列表
li1=Llist1()
for i in range(10):li1.append(i)
print('看看链表里有哪些成员:',end='')
li1.print()#print方法后curr指针就指向了尾节点
print(f'当前链表所指向的元素是:{li1.currValue()}')
print(f'链表的长度是:{li1.length()}')
li1.setFirst()
li1.next()
li1.next()
li1.next()
li1.remove()
print(f'看看删除之后的链表:',end='')
li1.print()
#看看循环功能是否正常
for i in range(random.randint(1,100)):li1.next()
print(f'我要循环一下,看看当前指针所指的数据是:{li1.currValue()}')
结果如下:
第三问
通过索引查找 | 插入 | 删除 | 前驱 | |
顺序表 | O(1) | O(n) | O(n) | O(1) |
单向链表 | O(n) | O(1) | O(1) | O(n) |
单向循环列表 | O(n) | O(1) | O(1) | O(1) |
第四问
利用第一问实现的单向循环列表模拟这个过程,每次淘汰掉一个人就作一个删除操作, 直到最后留下的一个人输出即可。
在这个过程中,我们用数字来模拟人,最后输出的是一个数字。
这里需要注意一个问题,第一问实现的单向循环链表是有头结点的,所以在模拟淘汰的时候需要注意避开这个头结点。即有两种情况:第一,在报数的时候遇到头结点就跳过。第二,如果报数结束刚好到了这个头结点, 那么也需要避开。
导入的文件名大家要根据自己情况修改。
from 单向循环链表 import Llist1
import matplotlib.pyplot as plt
#循环列表解法
#假设有n个人
#假设每隔m个人就淘汰 如果是报数的话,就要减一
def ysf(n,m):'''这里解释一下两个参数的意义,n表示有n个人,m表示每隔m个人就淘汰一个人。但是这里的m含义和题中的m含义有所不同:题中的m是报数报到m的那个人就淘汰,而这里的m是间隔m个,所以两者会相差1,输入时要记得转换一下即可'''peo=Llist1()for i in range(n):peo.append(i+1)#从一号开始peo.setFirst()while(peo.head.next!=peo.tail):for i in range(m):peo.next()if peo.curr.data==None:peo.next()if peo.curr==peo.tail:peo.next()peo.remove()return peo.tail.datay=[]
for i in range(1,201):y.append(ysf(i,1))
print(y)
plt.figure()
plt.plot(range(1,201),y)
plt.show()
结果展示:
规律为: f(n,m)=[f(n-1,m)+m]%n+1
时间复杂度为: O(n)
注:这里大家看情况修改自己的代码,因为每个人对隔m个人淘汰一个人的理解不一样(或者是报数到m就把那个人淘汰掉)这些细节都需要自己修改。同样规律的总结也会有所不同。
作者自己编写,如果错误,请不吝赐教。