什么是线性结构
Linear Structure
线性结构是一种有序数据项的集合,其中每个数据项都有唯一的前驱和后继
- 除了第一个没有前驱,最后一个没有后继。数据项之间只存在先后的次序关系
线性结构总有两端,在不同的情况下,称呼不同“左、右”“前、后”“顶、底”
两端的称呼不是关键,不同线性结构的关键区别在于数据项增减的方式
- 栈Stack
- 队列Queue
- 双端队列Deque
- 列表
栈抽象数据类型及Python实践
什么是栈Stack? ——托盘、书堆
- 一种有次序的数据项集合,在栈中,数据项的加入和移除都仅发生在同一端。这一端叫栈“顶top”,另一端叫栈“底base”
- 距离栈底越近的数据项,留在栈中的时间最长。称为“后进先出LIFO”:Last in First out
栈的特性:反转次序 ——进栈和出栈的次序正好相反
抽象数据类型Stack
抽象数据类型“栈”是一个有次序的数据集,每个数据项从“栈顶”一端加入到数据集中、从数据集中移除,栈具有后进先出的LIFO的特性
抽象数据类型“栈”定义为如下的操作
- Stack():创建一个空栈,不包含任何数据项
- push(item):将item加入栈顶,无返回值
- pop():将栈顶数据项移除,并返回,栈被修改
- peek():“窥视”栈顶数据项,返回栈顶的数
- isEmpty():返回栈是否为空栈
- size():返回栈中有多少个数据项
操作样例
用Python实现ADT Stack——abstract data type抽象数据类型
- 将ADT Stack实现为Python的一个Class
- 将ADT Stack的操作实现为Class的方法
- 由于Stack是一个数据集,选用数据集List来实现
- 一个细节:可以将List的任意一端(index=0或者-1)设置为栈顶,我们选用末端(index=-1)作为栈顶,这样栈的操作可以通过List的append和pop来实现
如何构造括号匹配识别算法?
从左到右扫描括号串,最新打开的左括号,应该匹配最先遇到的右括号,这种次序反转的识别,符号栈的特性
#用Python实现ADT Stack
class Stack:
def __init__(self):
self.items = []
def isEmpty(self):
return self.items == []
def push(self,item):
self.items.append(item)
def pop(self):
return self.items.pop()
def peek(self):
return self.item[len(self.items)-1]
def size(self):
return len(self.items)
#from pythonds.basic.stack import Stack
def parChecker(symbolString):
s = Stack()
balanced = True
index = 0
while index < len(symbolString) and balanced:
symbol = symbolString[index]
if symbol in "([{":
s.push(symbol)
else:
if s.isEmpty():
balanced = False
else:
top = s.pop()
if not matches(top,symbol):
balanced = False
index = index + 1
if balanced and s.isEmpty():
return True
else:
return False
def matches(open,close):
opens = "([{"
closers = ")]}"
return opens.index(open) == closers.index(close)
print(parChecker('((()))'))
print(parChecker('(()'))
直接安装pythonds模块:pip3 install pythonds
调用方法from pythonds.basic.stack import Stack
ADT Stack的另一个实现
如果我们把List的index=0作为栈顶,同样也可以实现Stack
不同的实现方案保持了ADT接口的稳定性,当性能有所不同。原来复杂度O(1),这个版本复杂度O(n)
栈的应用
例如,
十进制转换为二进制
采用“除以2求余数”的算法:将整数不断除以2,每次得到的余数就是由低到高的二进制位,得到的余数是从低到高的次序,而输出则是从高到低,所以需要一个栈来反转次序。
from pythonds.basic.stack import Stack
def divideBy2(decNumber):
remstack = Stack()
while decNumber > 0:
rem = decNumber % 2
remstack.push(rem)
decNumber = decNumber // 2
binString = ""
while not remstack.isEmpty():
binString = binString + str(remstack.pop())
return binString
print(divideBy2(35))
十进制转换为十六以下任意进制
from pythonds.basic.stack import Stack
def baseConverter (decNumber,basic):
digits = "0123456789ABCDEF"
remstack = Stack()
while decNumber > 0:
rem = decNumber % basic
remstack.push(rem)
decNumber = decNumber // basic
newString = ""
while not remstack.isEmpty():
newString = newString + digits[remstack.pop()]
return newString
print(baseConverter(25,2))
print(baseConverter(25,16))
值得学习的:准备了一个digits方便表示转化进制后的数
表达式转化
两步:
1.将中缀表达式转换为全括号形式
2.将所有的操作符移动到子表达式所在的左括号(前缀)或者右括号(后缀)处,代替之,再删除所有的括号
不需要全括号的算法:
通过观察中缀表达式A+BC,其对应的后缀表达式ABC+
- 操作数ABC的顺序没有改变
- 操作符的出现顺序,在后缀表达式中反转了
- 由于*的优先级比+高,所以后缀表达式中操作符的出现顺序与运算次序一致。
中缀转后缀时,在扫描到对应的第二个操作数之前,需要把操作符先保存起来
而这些暂存的操作符,由于优先级的规则,可能需要反转次序输出(用栈保存)
遇到左括号,要标记下,其后出现的操作符优先级提升了,一旦扫描到对应的右括号,就可以马上输出这个操作符
通用的中缀转后缀算法:流程
1.后面的算法描述中,约定中缀表达式是由空格隔开的一系列单词(token)构成
- 操作符单词包括*/±()
- 而操作数单词是单字母标识符A、B、C等
2.首先,创建空栈opstack用于暂存操作符,空表postfixList用于保存后缀表达式
3.将中缀表达式转换为单词(token)列表
4.从左到右扫描中缀表达式单词列表
- 如果单词是操作数,则直接添加到后缀表达式列表的末尾
- 如果单词是左括号“(”,则压入opstack栈顶
- 如果单词是右括号“)”,则反复弹出opstack栈顶操作符,加入到输出列表末尾,直到碰到左括号
- 如果单词是操作符“* / + -”,则压入opstack栈顶
但在压入之前,要比较其与栈顶操作符的优先级
如果栈顶的高于或等于它,就要反复弹出栈顶操作符,加入到输出列表末尾
直到栈顶的操作符优先级低于它5.中缀表达式单词列表扫描结束后,把opstack栈中的所有剩余操作符依次弹出,添加到输出列表末尾
6.把输出列表再用join方法合并成后缀表达式字符串,算法结束
from pythonds.basic.stack import Stack
def infixToPostfix(infixexper):
#用字典记录操作符优先级
prec = {}
prec["*"] = 3
prec["/"] = 3
prec["+"] = 2
prec["-"] = 2
prec["("] = 1
opStack = Stack()
postfixList = []
#解析表达式到单词列表
tokenList = infixexper.split()
for token in tokenList:
if token in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" or token in "0123456789":
postfixList.append(token)
elif token == '(':
opStack.push(token)
elif token == ')':
topToken = opStack.pop()
while topToken != '(':
postfixList.append(topToken)
topToken = opStack.pop()
else:
while (not opStack.isEmpty()) and \
(prec[opStack.peek()]) >= prec[token]:
postfixList.append(opStack.pop())
opStack.push(token)
while not opStack.isEmpty():
postfixList.append(opStack.pop())
#合成后缀表达式字符串
return "".join(postfixList)
print(infixToPostfix("( A + B + C )* D"))
值得学习的:操作符赋值比较优先级
举个例子:( A + B + C )* D
“(” 直接存入opStack
“A” 直接存入postfixList (A)
“+” 虽然opStack不是空的,但是+优先于(,所以存入opStack
“B” 直接存入postfixList (B)
“+” 因为opStack不是空的,且+和原来的+优先级一样,所以在opStack中弹出原来的+并在postifixList中加入这个+ (AB+),同时在opStack中存入新的+
“C”直接存入postfixList (AB+C)
“)” 弹出opStack最新存入的+不是(,所以在posfixList中加入这个+ (AB+C+),并弹出接下来的)是)不进行任何操作
“”现在opStack是空,所以将存入opStack
“D”直接存入postfixList (AB+C+D)
循环结束
当opStack不为空,将里面的*弹出加入postfixList
后缀表达式求值:流程
注意:先弹出的是右操作数,后弹出的是左操作数,这个对于-/很重要
1.创建空栈operandStack用于暂存操作数
2.将后缀表达式用split方法解析为单词(token)的列表
3.从左到右扫描单词列表
- 如果单词是一个操作数,将单词转换为整数int,压入operandStack栈顶
- 如果单词是一个操作符(* / + -),就开始求值,从栈顶弹出2个操作数,先弹出的是右操作数,后弹出的是左操作数,计算后将值重新压入栈顶
4.单词列表扫描结束后,表达式的值就在栈顶
5.弹出栈顶的值,返回
from pythonds.basic.stack import Stack
def postfixEval(postfixExpr):
operandStack = Stack()
tokenList = postfixExpr.split()
for token in tokenList:
if token in "0123456789":
operandStack.push(int(token))#操作数
else:#操作符
operand2 = operandStack.pop()
operand1 = operandStack.pop()
result = doMath(token,operand1,operand2)
operandStack.push(result)
return operandStack.pop()
def doMath(op,op1,op2):
if op == "+":
return op1 * op2
elif op == "/":
return op1 / op2
elif op == "+":
return op1 + op2
else:
return op1 - op2
print(postfixEval('4 5 6 * +'))
队列Queue:什么是队列?
队列是一种有次序的数据集合,其特征是新数据项的添加总发生在一端(通常称为“尾rear”端)而现存数据项的移除总发生在另一端(“首front端”)
原则:(FIFO:First-in first-out)先进先出先到先服务(first-come first-served)
抽象数据类型Queue由如下操作定义:
- Queue():创建一个空队列对象,返回值为Queue对象;
- enqueue(item):将数据项item添加到队尾,无返回值;
- dequeue():从队首移除数据项,返回值为队首数据项,队列被修改
- isEmpty():测试是否空队列,返回值为布尔值
- size():返回队列中数据项的个数
Python实现ADT Queue
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)
队列的应用
热土豆问题(约瑟夫问题):算法
用队列来实现热土豆问题的算法,参加游戏的人名列表,以及传土豆次数num,算法返回最后剩下的人名
1.模拟程序采用队列来存放所有参加游戏的人名,按照传递土豆方向从队首排到队尾(游戏时,队首始终是持有土豆的人)
2.模拟游戏开始,只需要将 队首的人出队,随即再到队尾入队,算是土豆的一次传递
3.传递了num次后,将队首的人移除,不再入队如此反复,直到队列中剩余1人
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))
值得学习的:总是队首的人拿土豆,这样方便一次循环后除掉拿土豆的人
打印任务:问题建模
对象:打印任务、打印队列、打印机
- 打印任务的属性:提交时间、打印页数
- 打印队列的属性:具有FIFO性质的打印任务队列
- 打印机的属性:打印速度、是否忙
过程:生成和提交打印任务
- 确定生成概率:实例为每小时会有10个学生提交2个作业,这样,概率是每180秒会有1个作业生成并提交
- 确定打印页数:实例是1~20页(概率相同)
过程:实施打印
- 当前的打印作业:正在打印的作业
- 打印结束倒计时:新作业开始打印时开始倒计时,回0表示打印完毕,可以处理下一个作业
模拟时间:
- 统一的时间框架:以最小单位(秒)均匀流逝的时间,设定结束时间
- 同步所有过程:在一个时间单位里,对生成打印任务和实施打印两个过程各处理一次
打印任务:模拟流程
1.创建打印队列对象
2.时间按照秒的单位流逝
- 按照概率生成打印作业,加入打印队列
- 如果打印机空闲且队列不空,则取出队首作业打印,记录此作业等待时间
- 如果打印机在忙,则按照打印速度进行1秒打印
- 如果当前作业打印完成,则打印机进入空闲
3.时间用尽,开始统计平均等待时间
4.作业的等待时间
- 生成作业时,记录生成的时间戳
- 开始打印时,当前时间减去生成时间即可
5.作业的打印时间
- 生成作业时,记录作业的页数
- 开始打印时,页数除以打印速度即可
from pythonds.basic.queue import Queue
import random
class Printer:
def __init__(self,ppm):
self.pagerate = ppm#打印速度
self.currentTask = None#打印任务
self.timeRemaining = 0#任务倒计时
def tick(self):#打印1秒
if self.currentTask != None:
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.getPages()* 60/self.pagerate
class Task:
def __init__(self,time):
self.timestamp = time#生成时间戳
self.pages = random.randrange(1,21)#打印页数
def getStamp(self):
return self.timestamp
def getPages(self):
return self.pages
def waitTime(self,currenttime):
return currenttime - self.timestamp#等待时间
def newPrintTask():
num = random.randrange(1,181)#1/180概率生成作业
if num == 180:
return True
else:
return False
def simulation(numSeconds,pagesPerMinute):#模拟
labprinter = Printer(pagesPerMinute)
printQueue = Queue()
waitingtimes = []
for currentSecond in range(numSeconds):
if newPrintTask():
task = Task(currentSecond)#时间流逝
printQueue.enqueue(task)
if (not labprinter.busy()) and (not printQueue.isEmpty()):
nexttask = printQueue.dequeue()
waitingtimes.append(nexttask.waitTime(currentSecond))
labprinter.startNext(nexttask)
labprinter.tick()
averageWait = sum(waitingtimes)/len(waitingtimes)
print("Average Wait %6.2f secs %3d tasks remaining."%(averageWait,printQueue.size()))
for i in range(10):
simulation(3600,5)
simulation(3600,5)
simulation(3600,10)
双端队列Deque:什么是Deque?
双端队列Deque集成了栈和队列的能力
两端可以称作“首”“尾”端,数据可以从首尾加入或移除
deque定义的操作如下:
- Deque():创建一个空双端队列
- addFront(item):将item加入队首
- addRear(item):将item加入队尾
- removeFront():从队首移除数据项,返回值为移除的数据项
- removeRear():从队尾移除数据项,返回值为移除的数据项
- isEmpty():返回deque是否为空
- size():返回deque中包含数据项的个数
Python实现ADT Deque+“回文词”判定
用双端队列
先将需要判断的词从队列加入deque
再从两端同时移除字符判定是否相同,知道deque中剩下0个或1个字符
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)
#from pythonds.basic.deque impot 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"))