目录
一、队列
1.什么是队列
队列是一种有次序的数据集合,其特征是新数据项的添加总发生在一端(通常称为“尾rear”端),而现存数据项的移除总发生在另一端(通常称为“首front”端)
当数据项加入队列,首先出现在队尾,随着队受数据项的溢出,它逐渐接近队首。
新加入的数据项必须在数据集末位等待,而等待时间最长的数据项则是队首,这种次序安排的原则称为(FIFO:First-in first-out)先进先出或“先到先服务first-come first-served”
队列仅有一个入口和一个出口,不允许数据项直接插入队中,也不允许从中间移除数据项
计算机科学中队列的例子:
# 1.打印队列
一台打印机面向多个用户/程序提供服务,打印速度比打印请求提交的速度要慢得多,有任务正在打印时,后来的打印请求就要排成队列,以FIFO的形式等待被处理。
# 2.进程调度
操作系统核心采用多个队列来队系统中同时运行的进程进行调度,进程数远多于CPU核心数
有些进程还要等待不同类型 I/O事件,调度原则综合了"先到先服务"及“资源充分利用”两个出发点
# 3.键盘缓冲
键盘敲击并不马上显示在屏幕上,需要有个队列性质的缓冲区,将尚未显示的敲击字符暂存其中,队列的先进先出性质则保证了字符的输入和显示次序一致性。
2.抽象数据类型Queue
Queue():创建一个空队列对象,返回值为Queue对象;
enqueue(item);将数据项item添加到队尾,无返回值;
dequeue():从队首移除数据项,返回值为队首数据项,队列被修改;
isEmpty():测试是否空队列,返回值为布尔值
size:返回队列中数据项的个数。
3.python实现ADT Queue
采用List来容纳Queue的数据项:
将List的首段作为队列尾端
List的末端作为队列手段
enqueue()复杂度为O(n)
dequeue()复杂度为O(1)
首尾倒过来的实现,复杂度也倒过来
class Queue:
def __init__(self):
self.items = []
def isEmpty(self):
return self.items == []
def enqueue(self,item):
self.items.insert(0,item)
def dequeue(self):
return self.items.pop()
def size(self):
return len(self.items)
4.举例 热土豆问题(约瑟夫问题)
“击鼓传花”的土豆版本
传烫手的热土豆,鼓声停的时候,手里有土豆的小孩就要出列
如果去掉鼓,改为传过固定人数,就成了“现代版”的约瑟夫问题
传说犹太人反叛罗马人,落到困境,约瑟夫和39人决定殉难,做成一圈儿,报数1~7,报到7的人由旁边杀死,结果约瑟夫给自己安排了个位置,最后活了下来……
from pythonds.basic.queue import Queue
def hotPotato(namelist,num):
simqueue = Queue()
for name in namelist:
simqueue.enqueue(name)
while simqueue.size()>1:
for i in range(num):
simqueue.enqueue(simqueue.dequeue())
simqueue.dequeue()
return simqueue.dequeue()
print(hotPotato(["Bill", "David", "Susan", "Jane", "Kent", "Brad"],7))
5.举例:打印队列
过程:生成和提交打印任务
确定生成概率:实例尾每小时会有10个学生提交的20个作业,这样,概率是每180秒会有1个作业生成并提交,概率为每秒1/180。
确定打印页数:实例是1~20页,那么就是1~20页之间概率相同。
from pythonds.basic.queue import Queue
import random
class Printer:
def __init__(self, ppm):
self.page_rate = ppm # 打印速度
self.currentTask = None # 打印任务
self.timeRemaining = 0 # 任务倒计时
def tick(self): # 打印1秒
if self.currentTask != None:
self.timeRemaining = self.timeRemaining - 1
if self.timeRemaining <= 0:
self.currentTask = None
def busy(self): # 打印忙?
if self.currentTask != None:
return True
else:
return False
def startNext(self,newtask): # 打印新作业
self.currentTask = newtask
self.timeRemaining = newtask.get_pages()*60/self.page_rate
class Task:
def __init__(self, time):
self.timestamp = time # 生成时间戳
self.pages = random.randrange(1,21) # 打印页数
def get_stamp(self):
return self.timestamp
def get_pages(self):
return self.pages
def waitTime(self,currenttime):
return currenttime-self.timestamp # 等待时间
def new_print_task():
num = random.randrange(1,181) # 1/181概率生成作业
if num == 180:
return True
else:
return False
def simulation(num_seconds, pages_per_minute): # 模拟
labprinter = Printer(pages_per_minute)
print_queue = Queue()
waitingtimes = []
for current_second in range(num_seconds): # 时间流逝
if new_print_task():
task = Task(current_second)
print_queue.enqueue(task)
if (not labprinter.busy()) and (not print_queue.isEmpty()):
next_task = print_queue.dequeue()
waitingtimes.append(next_task.waitTime(current_second))
labprinter.startNext(next_task)
labprinter.tick()
average_wait = sum(waitingtimes)/len(waitingtimes)
print('Average Wai#t %6.2f secs %3d tasks remaining.'%(average_wait,print_queue.size()))
# print(waitingtimes)
for i in range(10):
simulation(3600, 5)
simulation(3600, 10)
'''
为了队打印模式设置进行决策,我们用模拟程序来评估任务等待时间,通过两种情况模拟仿真结果的分析,我们认识到如果有那么多学生要拿着打印号的程序源代码赶去上课的话,那么,必须得牺牲打印质量,提高打印速度。
模拟系统对现实得仿真,在不耗费现实资源得情况下,有时候真实的实验是无法进行的,可以以不同得设定,反复多次模拟,来帮助我们进行决策。
'''
二、双端队列
1.什么是双端队列?
双端队列Deque是一种有次序得数据集,跟队列相似,其两端可以称作“首”“尾”端,但deque中数据项既可以从队列首加入,也可以从队尾加入;数据项也可以从两端移除。
某种意义上说,双端队列集成了栈和队列得能力。
但双端队列并不具有内在得LIFO或者FIFO特性,如果用双端队列来模拟栈或队列,需要由使用者自行维护操作得一致性
2.抽象数据类型Deque
Deque():创建一个空双端队列
addFront(item):将item加入队首
addRear(item):将item键入队尾
removeFront():从队首移除数据项,返回值尾移除得数据项
removeRear():从队尾移除数据项,返回值为移除得数据项
isEmpty():返回deque是否为空
size():返回deque中包含数据项得个数。
3.python ADT Deque
采用List实现:List下标0作为deque的尾端,List下表-1作为deque的首段
操作复杂度:addFront/removeFront O(1),addRear/removeRear O(n)
class Deque:
def __init__(self):
self.items = []
def isEmpty(self):
return self.items == []
def addFront(self,item):
self.items.append(item)
def addRear(self,item):
self.items.insert(0,item)
def removeFront(self):
return self.items.pop()
def removeRear(self):
return self.items.pop(0)
def size(self):
return len(self.items)
4.举例:“回文词”判定
如radar、madam、toot
中文“上海自来水来自海上”,“山东落花生花落东山”,“北京输油管油输京北”
用双端队列很容易解决“回文词”问题,先将需要判定的词从队尾加入deque,再从两端同时移除字符判定是否相同,直到deque中剩下0个或一个字符
from pythonds.basic.deque import Deque
def palchecker(aString):
chardeque = Deque()
for ch in aString:
chardeque.addRear(ch)
stillEqual = True
while chardeque.size() > 1 and stillEqual:
first = chardeque.removeFront()
last = chardeque.removeRear()
if first != last:
stillEqual = False
return stillEqual
print(palchecker('lsdkjfskf'))
print(palchecker('radar'))
三、无序表抽象数据类型及python实现
1.列表List:什么是列表?
在前面基本数据结构的讨论中,我们采用python list来实现了多种现行数据结构。列表list是一种简单强大的数据集结构,提供了丰富的操作接口。但并不是所有的编程语言都提供了List数据类型,有时候需要程序员自己实现。
无序表List的操作如下:
List():创建一个空列表
add(item):添加一个数据项到列表中,假设item原先不存在于列表中
remove(item):从列表中移除item,列表被修改,item原先应存在于表中
search(item):在列表中查找item,返回布尔值类型值
isEmpty():返回列表是否为空
size():返回列表包含了多少数据项
append(item):添加一个数据项到表末尾,假设item原先不存在于列表中
index(item):返回数据项在表中的位置
insert(pos,item):将数据项插入到位置pos,假设item原先不存在于列表中,同时原列表具有足够多个数据项,能让item占据位置pos
pop():从列表末尾移除数据项,假设原列表至少有1个数据项
pop(pos):移除位置为pos的数据项,假设原列表存在位置pos
为了实现无需表数据结构,可以采用链接表的方案。
虽然列表数据结构要求保持数据项的前后相对位置,但这种前后位置的保持,并不要求数据项一次存放在连续的存储空间
如下图,数据项存放位置并没有规则,但如果在数据项之间建立连接指向,就可以保持其前后相对位置。
第一个和最后一个数据项需要现实标记出来,一个是队首,一个是队尾,后面再无数据了。
2.链表实现:节点Node
链表实现的最基本元素是节点Node:
每个节点至少要包括2个信息:数据项本身,以及指向下一个节点的引用信息。
注意next为None的意义是没有下一个节点了,这个很重要。
class Node:
def __init__(self,initdata):
self.data = initdata
self.next = None
def getData(self):
return self.data
def getNext(self):
return self.next
def setData(self,newdata):
self.data = newdata
def setNext(self, newnext):
self.next = newnext
temp = Node(93)
temp.getData()
可以采用连接节点的方式构建数据集来实现无序表,链表的第一个和最后一个节点最重要。如果想访问到链表中的所有节点,就必须从第一个节点开始沿着链接遍历下去
所以无序表必须要有对第一个节点的引用信息。设立一个属性head,保存对第一个节点的引用空表的head为None。
class UnorderedList:
def __init__(self):
self.head = None
mylist = UnorderedList()
print(mylist.head) # None
随着数据项的加入,无序表的head始终指向链条中的第一个节点。注意!无序表mylist对象本身并不包含数据项(数据项在节点中),其中包含的head只是对首个节点Node的引用,判断空表的isEmpty()很容易实现。
return self.head == None
接下来,考虑如何实现想无序表中添加数据项,实现add方法。由于无序表并没有限定数据项之间的顺序,新数据项可以加入到原表的任何位置,按照现实的性能考虑,应添加到最容易加入的位置上。由链表结构我们直到,要访问整条链上的所有数据项,都必须从表头head开始沿着next链接逐个向后查找,所以添加新数据项最快捷的位置是表头,整个链表的首位置。
add方法按照有图的代码调用,形成的链表如下图:
31是最先被加入的数据项,所以成为链表中最后一个项。而54是最后被加入的,是链表第一个数据项。
mylist.add(54)
mylist.add(26)
mylist.add(93)
mylist.add(17)
mylist.add(77)
mylist.add(31)
size:从链条头head开始遍历到表尾同时用变量累加经过的节点个数。
def size(self):
current = self.head
count = 0
while cuttent != None:
count = count +1
current = current.getNext()
return count
从链表头head开始遍历到表尾,同时判断当前节点的数据项是否目标
def search(self,item):
current =self.head
found = False
while current != None and not found:
if current.getData() == item:
found = True
else:
current = current.getNext()
return found
remove(item)方法:
首先要找到item,这个过程跟search一样,但在删除节点时,需要特别的技巧。current指向的是当前匹配数据项的节点,而删除需要把前一个节点的next指向current的下一个节点。所以我们在search current的同时,还要维护前一个(previous)节点的引用。
找到item之后,current指向item节点,previous指向前一个节点,开始执行删除,需要区分两种情况:
1.current 是首个节点;
2.或者是位于里那条中间的节点。
def remove(self,item):
current = self.head
previous = None
found = False
while not found:
if current.getData() == item:
found = True
else:
previous = current
current = current.getNext()
if previous == None:
self.head = current.getNext()
else:
previous.setNext(current.getNext())
四、抽象数据类型:有序表OrderedList
有序表是一种数据项依照其某可比性质(如整数大小、字母表先后)来决定在列表中的位置。越“小”的数据项越靠近列表的头,越靠“前”
OrderedList():创建一个空的有序表
add(item):在表中添加一个数据项,并保持整体顺序,此项原不存在
remove(item):从有序表中移除一个数据项,此项应存在,有序表被修改
search(item):在有序表中查找数据项,返回是否存在
isEmpty():是否空表
size():返回表中数据项的个数
index(item):返回数据项在表中的位置,此项应存在
pop():移除并返回有序表中最后一项,表中应至少存在一项
pop(pos):移除并返回有序表中指定位置的数据项,此位置应存在。
以整数数据项为例,(17,26,31,54,77,93)的链表形式如图
有序表OrderedList实现,同样采用链表方法实现,Node定义相同,OrderedList也设置一个head来保存链表表头的引用
class OrderedList:
def __init__(self):
self.head = None
对于isEmpty/size/remove这些方法,与节点的次序无关,所以其实现跟UnorderdList是一样的。
search/add方法则需要有修改
# 有序表实现:search方法
在无序表的search中,如果需要查找的数据项不存在,则会从表头沿着next链接搜遍整个链表,直到表尾。对于有序表来说,可以利用链表节点有序排列的特性,来为search节省不存在数据项的查找时间。一旦当前节点的数据项大于所要查找的数据项,则说明链表后面已经不可能再有要查找的数据项,可以直接返回False。
def search(self,item):
current = self.head
found = False
stop = False
while current != None and not found and not stop:
if current.getData() == item:
found = True
else:
if current.getData() > item:
stop = True
else:
current = current.getNext()
return found
# 有序表实现:add方法
相比无序表,改变最大的方法是add,因为add方法必须保证加入的数据项添加在合适的位置,以维护整个链表的有序性。
比如在(17,26,54,77,93)的有序表中,加入数据项31,我们需要沿着链表,找到第一个比31大的数据项54,将31插入到54的前面
def add(self,item):
current = self.head
previous = None
stop = False
while current != None and not stop:
if current.getData() > item: # 发现插入位置
stop = True
else:
previous = current
current = current.getNext()
temp = Node(item)
if previous == None: # 插在表头
temp.setNext(self.head)
self.head = temp
else: # 插在表中
temp.setNext(current)
previous.setNext(temp)
五、链表实现的算法分析
对于链表复杂度的分析,主要是看相应的方法是否设计到链表的遍历
对于一个包含节点数为n的链表:
isEmpty是O(1),因为仅需要检查head是否为None
size是O(n),因为除了遍历到表尾,没有其他办法得知节点的数量
search/remove以及有序表的add方法,则是O(n),因为涉及到链表的遍历,按照概率其平均操作的次数是n/2,无序表的add方法是O(1),因为仅需要插入到表头。
链表实现的List,跟Python内置的列表数据类型,在有些相同方法的实现上的时间复杂度不同。主要是因为python内置的列表数据类型是基于顺序存储来实现的,并进行了优化。
六、线性结构小结
线性数据结构Linear DS将数据项以某种线性的次序组织起来。
栈Stack维持了数据项后进先出LIFO的次序,stack的基本操作包括push,pop,isEmpty
书写表达式的方法有前缀prefix、中缀infix和后缀postfix三种,由于栈结构具有次序反转的特性,所以栈结构适合用于开发表达式求值和转换的算法
队列Queue维持了数据项先进先出FIFO的次序,queue的基本操作包括enqueue,dequeue,isEmpty
“模拟系统”可以通过一个对现实世界问题进行抽象建模,并加入随机数动态运行,为复杂问题的决策提供各种情况的参考,队列queue可以用来进行模拟系统的开发
双端队列Deque可以同时具备栈和队列的功能,deque的主要操作包括addFront,addRear,removeFront,removeRear,isEmpty
列表list是数据项能够维持相对位置的数据集,链表的实现,可以保持列表维持相对位置的特点,而不需要连续的存储空间
链表实现时,其各种方法,对链表头部head需要特别的处理。
七、双链表
双链表的每个节点有两个指针:一个指向后一个节点,另一个指向前一个节点。
class Node:
def __init__(self,item=None):
self.item = item
self.next = None
self.prior = None
# 双链表节点的插入 如下图
1. p.next = curNode.next
2. curNode.next.prior = p
3. p.prior = curNode
4. curNode.next = p
# 双链表节点的删除
1. p = curNode.next
2. curNode.next = p.next
3. p.next.prior = curNode
4. del p