2基本结构(上)——数据结构与算法Python版学习笔记

什么是线性结构

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"))
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值