数据结构与算法(Python)

一. Python数据类型的性能

1.list 列表

  • 列表list所包含的方法
#1 添加:
list.append(a_new_element) #在列表末尾添加新的对象 O(1)
list_a.extend(list_b)#在列表末尾一次性追加另一个序列中的多个值(用新列表扩展原来的列表)
list.insert(index,obj)#将指定对象插入列表的指定位置 O(n)#2 删除
list.pop(index)#移除某位置上的元素,默认值为-1()即删除最后一个位置上的元素。因此pop()为O(1),而pop(i)为O(n)【返回值为pop掉的元素】
list.remove(element)#移除列表中某个值的第一个匹配项#3 更新
list[i]=new_element#4 正查
list[i]
list[i:j]#5 反查
list.index(element)#从列表中找出某个值第一个匹配项的索引位置
list.count(element)#统计某个元素在列表中出现的次数#6 其他
list.reverse()#反向列表中元素
list.sort(cmp=None,key=None,reverse=False)#下一个框阐释
#cmp -- 可选参数, 如果指定了该参数会使用该参数的方法进行排序。
#key -- 主要是用来进行比较的元素,只有一个参数,具体的函数的参数就是取自于可迭代对象中,指定可迭代对象中的一个元素来进行排序。
#reverse -- 排序规则,reverse = True 降序, reverse = False 升序(默认)。#进行一个key的例子解释def sort_by_second(element):return element[1]
list=[(1,2),(2,3),(3,4),(4,1)]
list.sort(key=sort_by_second)
print(list)
#[(4,1),(1,2),(2,3),(3,4)]
  • 4种生成前n个整数列表的方法

    #1 append方法
    def m1():list=[]for i in range(n):list.append(i)#2 循环连接列表
    def m2():list=[]for i in range(n):list=list+[i]#3 range函数+list函数
    def m3():list=list(range(n))#4 列表推导式
    def m4():list=[i for i in range(n)]
    

####2.dict 字典

二 . 线性结构

(一)栈Stack

1.基本操作方法与python实现
stack=Stack()#创建一个空栈stack.push(item)#将item加入栈顶,无返回值
stack.pop()#将栈顶数据移除,并返回移除的值
stack.peek()#返回栈顶数据,不改变栈
stack.isEmpty()#判断栈是否为空,返回值True or False
stack.size()#返回栈中有多少个数据项
#1 列表尾端作为栈顶
class Stack:def __init__(self):self.items=[]def isEmpty(self):return self.items==[]def push(self,item):self.item.append(item)def pop(self,item):return self.items.pop()def peek(self):return self.items[len(self.items)-1]def size(self):return len(self.items)#2 列表始端作为栈顶
class Stack:def __init__(self):self.items=[]def isEmpty(self):return self.items==[]def push(self,item):self.items.insert(0,item)def pop(self):return self.items.pop(0)def peek(self):return self.item[0]def size(self):return len(self.item)

2.栈的应用
①简单括号匹配
  • 单类括号匹配
from pythonds.basic.stack import Stack#自己写的,但有问题,只是大体实现逻辑,并没有考虑冗余,因此逻辑是不正确的
def ParChecker(item):s=Stack()count=0i=0while i<len(item) :if item[i]=='(':s.push(i)count+=1elif item[i]==')':s.pop()count-=1i+=1#标答
def ParChecker(item):s=Stack()balance=Truei=0while i<len(item) and balance:a=item[i]if a=='(':s.push(a)else:if s.isEmpty():balance=Falseelse:s.pop()#else里的这一步内判断可以说一方面,是pop的必要考量,另一方面,它同时可以作为是否有右括号冗余的情况,因此这里通过balance的True or False进行记录i+=1if balance and s.isEmpty():return True#由于前面的那个,即使balance=False让那个循环停下了,但那仅仅是跳过循环,这里返回值时必须综合考虑左括号冗余和右括号冗余,也就是上面and连接的两个条件。else:return False
  • 通用括号匹配

    from pythonds.basic.stack import Stackdef match(open,closer):opens="([{"closers=")]}"if opens.index(open)==closers.index(closer):return Trueelse:return False#或直接: return opens.index(open)==closers.index(closer)def ParChecker(item):s=Stack()balance=Truei=0while i<len(item) and balance:a=item[i]opens="([{"if a in opens:s.push(a)else:if s.isEmpty():balance=Falseelse:if match(a,s.peek()):s.pop()else:balance=Falsei+=1                if balance and s.isEmpty():return Trueelse:return False
②十进制转化为二进制
③表达式转换(中缀表达式转为后缀表达式)
from pythonds.basic.stack import Stackdef change(thing):#设置新栈和新列表用来操作opStack=Stack()postfixlist=[]#给标点符号划分阵营marks="(+-*/"sup="*/"inf="+-"list=thing.split()for i in list:#1 若为运算符(非右括号)if i in marks:                            #若为高优先级,则前面的运算符只有也是高优先级运算符才出栈进列表if i in sup:if opStack.peek() in sup:postfixlist.append(opStack.pop())#若为低优先级,则前面的运算符只要不是括号就出栈进列表elif i in inf:if opStack.peek() in inf or opStack.peek() in sup:postfixlist.append(opStack.pop())#运算符入栈opStack.push(i)#2 若是右括号,那么左括号后的全部出栈进列表elif i==')':while opStack.peek()!='(':postfixlist.append(opStack.pop())opStack.pop() #左括号纯出栈#3 若为运算量else:postfixlist.append()print(postfixlist)
#    postfix=''.join(postfixlist)
#    return postfixitem=input('请输入中缀表达式:')
change(item)

######④后缀表达式求值

from pythonds.basic.stack import Stackdef calculate(m):opeStack=Stack()list=m.split()marks="+-*/"for i in list:if i not in marks:opeStack.push(int(i))else:oper2=opeStack.pop()oper1=opeStack.pop()# c=b+i+a 是不对的result=doMath(oper2,oper1,i)opeStack.push(result)return opeStack.pop()def doMath(oper2,oper1,op):if op=='+':return oper1+oper2 elif op=='-':return oper1-oper2elif op=='*':return oper1*oper2elif op=='/':return oper1/oper2print(calculate(input('请输入要计算的后缀表达式:')))

(二)队列Queue

1.队列的实现
#1 队列的方法
queue=Queue() #创建一个空队列queue.isEmpty()#判断队列是否为空,返回值True or False
queue.size()#返回队列元素个数
queue.enqueue(item)#将新元素加入队列末尾
queue.dequeue(item)#将队列开头的第0个元素移除
#2 队列的代码实现
class Queue():def __init__(self):self.items=[]def isEmpty(self):return self.items==[]def enqueue(self,item):self.items.append(item)#self.items.insert(0,item)def dequeue(self):return self.items.pop(0)#return self.items.pop()def size(self):return len(self.items)
2.队列的应用
①热土豆(约瑟夫问题)【击鼓传花】

​ 在实现过程中,先是使用了“相对运动” “转换参考系”的思想,然后又将旋转的圈拆开,拆成了一个队列结构,从而将例子抽象出模型。

from pythonds.basic.queue import Queuedef hotPotato(namelist,num):#创建并将人名写入队列queue=Queue()for i in namelist:queue.enqueue(i)#循环进行,直到最后只剩一人while queue.size()>1:   for i in range(num):queue.enqueue(queue.dequeue())#反复移除队首加入队尾,即转圈操作queue.dequeue()#上述转圈操作进行num次后,杀掉一个,返回循环开始处return queue.dequeue()#返回最后队列中唯一的一个人
②打印任务【模拟仿真】
from pythonds.basic.queue import Queue
import random#1 定义Printer类
class Printer:def __init__(self,ppm):self.pagerate=ppmself.currenttask=Noneself.timeremaining=0#模拟在时间流逝一秒期间printer的操作def tick(self):if self.currenttask!=None:self.timeremaining=self.timeremaining-1  #代表时间流逝一秒,即时间与printer的接口if self.timeremaining<=0:self.currenttask=None  #作业执行完毕的逻辑#判断是否忙碌def busy(self):if self.currenttask!=None:return Trueelse:return False#执行新的打印任务def startNext(self,newtask):self.currenttask=newtaskself.timeremaining=newtask.getPages()*60/self.pagerate#2 定义Task类
class Task():def __init__(self,time):self.timestamp=time #需要输入的参数,即任务创建的时间self.pages=random.randrange(1,21) #随机生成task的大小def getStamp(self):return self.timestampdef waitTime(self,currentsecond):return currentsecond-self.timestampdef getPages(self):return self.pages#3 生成作业(实际并不是在生成作业,只是表达了一下概率,用于模拟过程中的if判断)
def newPrintTask():num=random.randrange(1,141)if num==140:return Trueelse:return False#4 模拟运行过程
def simulation(numseconds,ppm):printer=Printer(ppm) #实例化printerpq=Queue() #实例化queuewaitingtime=[] #空列表用来存储每个任务的等待时间,优点是即可以存放每一个的时间,又可以计数,便于统计平均值#通过循环来模拟时间流逝,每一次循环代表一秒,用所在循环的序数作为时间戳for currentsecond in numseconds:#① 生成任务并加入队列if newPrintTask():   # 1/180概率判断task=Task(currentsecond)  #这里才是真正的生成新任务(同时输入当下的时间戳(即循环序数))pq.enqueue(task) #入队列#② 从队列取出任务并交给打印机,同时记录下它的等待时间if (not printer.busy()) and (not pq.isEmpty()):nexttask=pq.dequeue() #出队列waitingtime.append(nexttask.waitTime(currentsecond)) #记录等待时间printer.startNext(nexttask) #进入printer#③ printer进行每秒钟它要做的事情printer.tick()#获得平均等待时间average_waitingtime=sum(waitingtime)/len(waitingtime)print('平均等待时间为:'+average_waitingtime)

(三)双端队列

1.基本操作方法与python实现
#基本操作方法
deque=Deque()
deque.addFront()
deque.addRear()
deque.removeFront()
deque.removeRear()
deque.isEmpty()
deque.size()
#双端队列的python实现
class Deque:def__init__(self):self.items=[]def isEmpty(self):return self.items==[]def size(self):return len(self.items)def addFront(self,item):self.items.insert(0,item)def addRear(self,item):self.items.append(item)def removeFront(self):return self.items.pop(0)def removeRear(self):return self.items.pop()
2.队列的应用
回文词的判定
from pythonds.basic.deque import Dequedef parChecker(word):deque=Deque()for i in word:deque.addRear(i)if deque.size()%2=0:return isMatch(deque.size())       else:return isMatch(deque.size()-1)def isMatch(num):match=Truewhile j<=num/2 and match:a=deque.removeFront()b=deque.removeRear()if a!=b:match=Falsej+=1return match#是这样,不过可以不用判断是否是偶数,然后用j+=1,而是只要把条件判断变成deque.size()>0就可啦,即以下
def parChecker(word):deque=Deque()for i in word:deque.addRear(i)match=Truewhile deque.size()>0 and match:a=deque.removeFront()b=deque.removeRear()if a!=b:match=Falsej+=1return match

####(四)列表List

​ Python内置的列表,是在物理结构上连续的无序表,这是出于使更常用的功能时间复杂度更低的目的形成的。

​ 除此之外,这里我们考虑写出无序表和有序表的链表实现。

1.无序表的链表实现
#无序表的操作#1 基本操作
list=List()
list.isEmpty()
list.size()
list.add(item)
list.remove(item)
list.search(item) # 在列表中查找item,返回布尔值True or False#2 专属操作
list.index(item)
list.append(item)
list.insert(pos,item)
list.pop()
list.pop(pos)
#无序表的实现#1 结点实现
class Node:def __init__(self,initdata):self.data=initdataself.next=Nonedef getData(self):return self.datadef getNext(self):return self.nextdef setData(self,newdata):self.data=newdatadef setNext(self,newnext):self.next=newnext#2 链表实现    class UnorderedList:def __init__(self):self.head=Nonedef isEmpty(self):return self.head==Nonedef add(self,item):temp=Node(item)#以下两句的次序非常重要temp.setNext(self.head)self.head=tempdef size(self):current=self.headcount=0while current!=None:current=current.getNext()count+=1return countdef search(self,item):current=self.headfound=Falsewhile current!=None and not found:if current.getData()==item:found=Truecurrent=current.getNext()return found# 乱入式展开一下:这里其实是一个查找算法,然后结合我在算法营学到的一点点知识,其实这个循环可以有多种写法
#1
while current!=None and not found:    if current.getData()==item:found=Truecurrent=current.getNext()
#2
while current!=None and not found:   if current.getData()==item:found=Trueelse:current=current.getNext()#这两种写法,总的来说,都可以使它在查找到item的时候停止查找,只不过第二种的current停留在对应的那个上,而第一种的current停留在了下一个即current.getNext()上,也就是比第二种多走了一步。def remove(self,item):current=self.headprevious=Nonefound=Falsewhile not found:if current.getData()==item:found=Trueelse:previous=currentcurrent=current.getNext()if previous==None:self.head=current.getNext()else:previous.setNext(current.getNext())# 注意:这一句如果写成previous.getNext()=current.getNext() 是不对的!!(这种赋值是赋到current上时用,不修改链表真实结构,但如果真的要对链表进行修改,必须用Node的方法)
# 你看,这个函数的实现就必须用if else(即上面说的第二种类型的写法)才能防止它走过了。
2. 有序表的链表实现
class OrderedList():
#1 和无序表相同的方法def __init__(self):self.head=Nonedef isEmpty(self):return self.head==Nonedef size(self):current=self.headcount=0while current!=None:current=current.getNext()count+=1return countdef remove(self,item):current=self.headprevious=Nonefound=Falsewhile not found:if current.getData()==item:found=Trueelse:previous=currentcurrent=current.getNext()#考虑是在表头还是中间移除if previous==None:self.head=current.getNext()else:previous.setNext(current.getNext())#2 和无序表不同的方法#1.1 搜索:可以在恰当的时候停下来了def search(self.item):current=self.headprevious=Nonefound=Falsestop=Falsewhile current!=None and not found and not stop:if current.getData()==item:found=Trueelse:if current.getData>item:stop=Trueelse:previous=currentcurrent=current.getNext()return found#1.2 加入:要先搜索到合适的位置再进行加入(这时就要像remove一样考虑在head处还是在中间了)def add(self,item):current=self.headprevious=Nonestop=Falsewhile current!=None and not stop:if current.getData()>item:stop=Trueelse:previous=currentcurrent=current.getNext()#考虑是在表头还是中间加入temp=Node(item)if previous==None:temp.setNext(current) # temp.setNext(self.head)self.head=tempelse:temp.setNext(current)previous.setNext(temp)

三. 递归

(一)递归可视化

递归算法“三定律”:

  1. 递归算法必须具备基本结束条件
  2. 递归算法必须逐渐减小规模,改变状态,向基本结束条件演进
  3. 递归算法必须要调用自身
1.螺旋
import turtle
t=turtle.Turtle()def drawSpiral(t,linelen):if linelen>0:t.forward(linelen)t.right(90)drawSpiral(t,linelen-5)drawSpiral(t,100)
turtle.done()
2.分形树
import turtledef Tree(branchlen):if branchlen>5: # 树干太短不画,即递归结束条件#画树干t.forward(branchlen)#递归调用画右边小树t.right(20)Tree(t,branchlen-15)#递归调用画左边小树t.left(40)Tree(t,branchlen-15)#退回原位置  (这一步回退非常重要!勿漏!无论是从大图景还是从细枝末节都能意识到这里必须回退!)t.right(20)t.backward(branchlen)t=turtle.Turtle()
t.left(90)
t.penup()
t.backward(100)
t.pendown()t.pencolor('green')
t.pensize(2)
Tree(75)
t.hideturtle()
turtle.done()
3.谢尔宾斯基三角
import turtle#先定义函数实现三角形的涂画
def drawTriangle(points,color):t.fillcolor=(color)t.penup()t.goto(points['top'])t.pendown()t.begin_fill()t.goto(points['left'])t.goto(points['right'])t.goto(points['top'])t.end_fill()#定义函数实现取边的中点
def getMid(p1,p2):return ((p1[0]+p2[0])/2,(p1[1]+p2[2])/2)#定义递归函数
def sierpinski(degree,points):colormap=['blue','red','green','white','yellow','orange']drawTriangle(points,colormap[degree]) #实现的操作if degree>0: #递归结束条件#调用自身sierpinski(degree-1,{'left':points['left'],'top':getMid(points['left'],points['top']),   'right':getMid(points['left'],points['right'])})sierpinski(degree-1,{'left':getMid(points['left'],points['top']),'top':points['top'],   'right':getMid(points['top'],points['right'])})sierpinski(degree-1,{'left':getMid(points['left'],points['right']),'top':getMid(points['top'],points['right']),   'right':points['right']})t=turtle.Turtle()
points={'left':(-200,-100),'top':(0,200),'right':(200,-100)}
sierpinski(5,points)
turtle.done()

(二)递归的应用

1.汉诺塔
#一个不完善的尝试
stack1=Stack()
stack2=Stack()
stack3=Stack()
stacks=[stack1,stack2,stack3]H(3,0,1,2):def execute(x1,x2,x3): s1=stacks[x1]s2=stacks[x2]s3=stacks[x3]s2.push(s1.pop())s3.push(s1.pop())s3.push(s2.pop())s2.push(s1.pop())s1.push(s3.pop())s2.push(s3.pop())s2.push(s1.pop())def H(n,x1,x2,x3):if not s3.isEmpty():s2.push(s3.pop())H(n-1,x2,x1,x3)
#应该是正确答案吧
def moveTower(height,fromPole,withPole,toPole):if height>=1:moveTower(height-1,fromPole,toPole,withPole)moveDisk(fromPole,toPole)moveTower(height-1,withPole,fromPole,toPole)def moveDisk(fromPole,toPole):toPole.push(fromPole.pop())fromPole=Stack()
withPole=Stack()
toPole=Stack()
moveTower(5,fromPole,withPole,toPole)

​ 总结一下自己这个题盲写代码失败的原因:其实整体逻辑和思路都想通了,甚至已经在纸上写出了和正确答案一模一样的伪代码了,但是问题出在:一是对各个柱子之间的规律没有得到格式化的把握,总觉得是开头有个特殊,然后后面是反复交换,是不对的规律;二是初始项想错了,理应是从第一项开始就好了,我却偏偏以为第三项才能真正体现逻辑,其实这个逻辑从第一项开始就已经顺理成章了,所以是我低估了递归的能力;第三点就是编程经验不足,伪代码能写出来但代码写的很磕巴,说明需要多练,继续积累经验。

2. 探索迷宫
#1 读取迷宫
class Maze:def __init__(self,mazeFileName):mazeFile=open(mazeFileName,'r')self.mazelist=[]for line in mazeFile:rowlist=[]rowsInMaze=0for ch in line[:-1]:columnsInMaze=0rowlist.append(ch)if ch=='s':self.startRow=rowsInMazeself.startCol=columnsInMazecolumnsInMaze+=1self.mazelist.append(rowlist)   rowsInMaze+=1#2 完善Maze class(辅助的动画过程)t #一个作图的海龟
drawMaze() #绘制出迷宫的图形,墙壁用实心方格绘制
updatePosition(row,col,val) #更新海龟的位置,并做标注
isExit(row,col) #判断是否是出口#3 探索迷宫
def searchFrom(maze,startRow,startCol):#如果所到达的这个点已有val,那么根据不同的值,会有不同的标记maze.updatePosition(startRow,startCol)if maze[startRow][startCol]==OBSTACLE:return Falseif maze[startRow][startCol]==TRIED or maze[startRow][startCol]==DEAD_END:return Falseif maze.isExit(startRow,startCol):maze.updatePosition(startRow,startColumn,PART_OF_PATH)return True#如果到达的点没有val,那么就先标记为TRIED,然后去进行下一层的探寻,直到出现上述某种遇到已有val的情况maze.updatePosition(startRow,startCol,TRIED)found=searchFrom(maze,startRow-1,startCol) or searchFrom(maze,startRow+1,startCol) or searchFrom(maze,startRow,startCol-1) or searchFrom(maze,startRow,startCol+1)    if found:maze.updatePosition(startRow,startCol,PART_OF_PATH)else:maze.updatePosition(startRow,startCol,DEAD_END)return found
3.递归方式实现圆括号匹配
def match(s,n=0):if s:if s[0]=='(':n=1else:n=if n<0:return Falsereturn match(s[1:],n)else:return n==0

(三)找零兑换问题

1.贪心策略
def change(money,max_,med_,min_):a=money/max_x1=money%max_b=x1/med_x2=x1%med_c=x2/min_sum=a+b+c
2.递归
#1 基础版本
def recMc(coinvaluelist,change):mincoins=changeif change in coinvaluelist:return 1else:for i in [c for c in coinvaluelist if c<=change]:numcoins=1+recMc(coinvaluelist,change-i)if numcoins<mincoins:mincoins=numcoinsreturn mincoinsprint(recMc([1,5,10,25],63))#缺点:会有非常多的重复,比如这里我们知道了下一步是15,然后后面连接还有好多步,但我们会遇到很多个下一步是15的情况,然后就造成了大量的重复计算,导致时间耗费超级多#2 改进版本(用一张表来记录,以消除重复,极大地提高了递归调用次数)
def recMc(coinvaluelist,change,knownResults):mincoins=changeif change in coinvaluelist:knownResults[change]=1return 1elif knownResults[change]>0:return knownResults[change]else:for i in [c for c in coinvaluelist if c<=change]:numcoins=1+recMc(coinvaluelist,change-i,knownResults)#下面这三行还是很重要的,它是change-1,change-5,change-10,change-25这几个各循环一次,然后如果更小,就取代,不是更小的话就还是原来那个,这样就可以实现得到最小数目的啦if numcoins<mincoins:mincoins=numcoinsknownResults[change]=mincoins #这一句好像是可以倒退4格(或者8格?可以吗我有点想不太清楚)return mincoins
print(recMc([1,5,10,25],63,[0]*64))
#中间结果的记录可以很好解决找零兑换问题,实际上,这种方法还不能称为动态规划,而是叫做memorization(记忆化、函数值缓存)的技术提高了递归解法的性能。
3.动态规划
#1 基础版
def dpMakeChange(coinvaluelist,change,mincoins):#个人目前感觉,动态规划相较于递归就在于,它不是循环调用自身,而是在这一层range的循环上从小到大规划for cents in range(1,change+1):coincount=centsfor i in [c for c in coinvaluelist if c<=cents]:#下面这两行,也是分别处理cents-1,cents-5,cents-10,cents-21之类的这种循环,然后最终得到最小的if mincoins[cents-i]+1<coincount:coincount=mincoins[cents-i]+1mincoins[cents]=coincountreturn mincoins[change]print(dpMakeChange([1,5,10,21,25],63,[0]*64))#2 PLUS版
def dpMakeChange(coinvaluelist,change,mincoins,coinUsed):for cents in range(change+1):coincount=centsnewcoin=1for i in [c for c in coinvaluelist if c<=cents]:if mincoins[cents-i]+1<coincount:coincount=mincoins[cents-i]+1newcoin=imincoins[cents]=coincountcoinUsed[cents]=newcoinreturn mincoins[change]def printCoins(coinUsed,change):coin=changewhile coin>0:thisCoin=coinUsed[coin]print(thisCoin)coin=coin-thisCoin

​ 动态规划中最主要的思想是:从最简单的情况开始到达所需找零的循环,其每一步都是依靠以前的最优解来得到本步骤的最优解,直到得到答案。

(四)博物馆大盗问题

1.递归解法
tr={(2,3),(3,4),(4,8),(5,8),(9,10)}
max_w=20
m={}
def theif(tr,w):if tr==set() or w==0:m[tuple(tr),w]=0return 0elif (tuple(tr),w) in m:return m[tuple(tr),w]else:vmax=0for t in tr:if t[0]<=w:v=thief(tr-{t},w-t[0])+t[1]vmax=max(vmax,v)m[tuple(tr),w]=vmaxreturn vmaxprint(theif(tr,max_w))
2.动归解法
tr=[None,{'w':2,'v':3},{'w':3,'v':4},{'w':4,'v':8},{'w':5,'v':8},{'w':9,'v':10}]
max_w=20
m={(i,w):0 for i in range(len(tr))for w in range(max_w+1)}
for i in range(1,len(tr)):for w in range(max_w+1):if tr[i]['w']>w:m[(i,w)]=m[(i-1,w)]else:m[(i,w)]=max(m[(i-1,w)],m[(i-1,w-tr[i]['w']+tr[i]['v'])
print(m[len(tr)-1,max_w])

(五)小结

  • 某些情况下,递归可以代替迭代循环;此外,递归算法通常能够跟问题的表达自然契合。
  • “记忆化/函数值缓存”可以通过附加存储空间以记录中间计算结果,来有效减少重复计算。(其实这样改进后它就有点类似于动归的思想了,并且有的时候这样比动归还高效,因为动归从小到大,全都算了填到表里,但递归+记录只算它需要的部分,当然只是说有这种时候啦)
  • 如果一个问题最优解包括规模更小的相同问题的最优解,就可以用动态规划来解决。

四. 排序与查找

(一)顺序查找 (Sequential Search)

1.无序表
def sequentialSearch(alist,item):pos=0found=Falsewhile pos<len(alist) and not found:if alist[pos]==item:found=Trueelse:pos+=1return found
2.有序表
def sequentialSearch(alist,item):pos=0found=Falsestop=Falsewhile pos<len(alist) and not found and not stop:if alist[pos]==item:found=Trueelse:if alist[pos]>item:stop=Trueelse:pos+=1return found

然而,要注意的是,有序表只是能够在某些情况下相比无序表提前结束循环,但是并不能改变顺序查找算法时间复杂度的数量级,即仍然是O(n)。

(二)二分查找 (Binary Search) 【只适用于有序表】

#1 常规实现
def binarySearch(alist,item):first=0last=len(alist)-1found=Falsewhile first<=last and not found:midpoint=(first+last)/2if alist[midpoint]==item:found=Trueelse:if alist[midpoint]>item:last=midpoint-1else:first=midpoint+1return found#2 分治策略——递归法
def binarySearch(alist,item):if len(alist)==0:return Falseelse:midpoint=len(alist)//2if alist[midpoint]==item:return Trueelse:if alist[midpoint]>item:return binarySearch(alist[:midpoint],item)else:return binarySearch(alist[midpoint+1:],item)

​ 通过一点很常规的计算,我们可以知道这个算法本身的时间复杂度是 O(logn) ,但是要注意如果是使用第二种递归,那个切片的时间复杂度是O(n),所以我们就只要知道它是可以递归的就可以了,实际上使用的还是第一个逻辑更好一点,其实我们递归也可以不切片,我们这里用切片只是为了让它更有python的味道而已啦。另外就是,由于它只适用于有序表,所以我们在实际问题中还要看,如果是无序表,根据你不同功能的使用情况及频率,值不值得为了它先排序,所以算法本身的时间复杂度是一方面,最后进行统筹规划还是很重要的。

(三)冒泡和选择排序算法

1. 冒泡排序 (BubbleSort)
#1 标准版
def bubbleSort(alist):for passnum in range(len(alist)-1,0,-1):for i in range(passnum):if alist[i]>alist[i+1]:#大部分程序设计语言的序错交换实现方式是以下三行的逻辑temp=alist[i]alist[i]=alist[i+1]alist[i+1]=temp#不过针对python语言有它自己的交换方式#alist[i],alist[i+1]=alist[i+1],alist[i]alist=[54,26,93,17,77,31,44,55,20]
bubbleSort(alist)
print(alist)
  • 时间复杂度O(n²)

  • 冒泡排序通常作为时间效率较差的排序算法,来作为其他算法的比较基准。它差就差在必须要进行多次对比和交换,但其中其实很多次对比和交换操作是无效的。

  • 但它的优点一个是无需任何额外的存储空间开销,另一个是因为它只涉及相邻两个数据的比对,所以对于非常多的数据结构,它都可以很好地处理,适应性比较广,比如无论是顺序存储还是链式存储,都是可以用的,但是其他很多排序算法在链表都是无法使用的。

  • 可以通过改进,监测本次是否发生了交换,如果没有发生交换的话,则说明排序已经完成了,可以提前终止。

    #2 改进版
    def shortBubbleSort(alist):exchanges=True #主要区别其实就是这里加了这么一个变量passnum=len(alist)-1while passnum>0 and exchanges:for i in range(passnum):exchanges=Falseif alist[i]>alist[i+1]:exchanges=Truetemp=alist[i]alist[i]=alist[i+1]alist[i+1]=temppassnum=passnum-1alist=[20,30,40,90,50,60,70,80,100,110]
    shortBubbleSort(alist)
    print(alist)
    
2.选择排序 (Selection Sort)
def selectionSort(alist):for fillslot in range(len(alist)-1,0,-1):positionIfMax=0for i in range(1,fillslot+1):if alist[i]>alist[i+1]:positionOfMax=itemp=alist[fillslot]alist[fillslot]=alist[positionOfMax]alist[positionOfMax]=temp#选择排序实际是对冒泡排序进行了改进,冒泡排序的内循环是交换,而选择排序的内循环是查找并记录,因此它虽然查找的时间复杂度还是O(n²),但交换的复杂度变成了O(n),所以是有了一定程度的改善叭。

(四)插入排序算法 (Insertion Sort)

​ 插入排序时间复杂度还是O(n²),但算法思路与冒泡排序、选择排序不同,它始终维持一个已排好序的子列表,其位置始终在列表的前部,然后逐步扩大这个子列表直到全表,比较类似于扑克牌整理扑克的方式。

def insertionSort(alist):for index in range(1,len(alist)):currentvalue=alist[index] #记录下这个待插入的值,这还是很重要的,因为它是一个固定的值,必须保存下来position=index #因为index是外层循环的循环变量,内层循环只是从index开始进行循环,并且不能修改到index的值,所以就另开一个position变量来进行内层循环了while position>0 and alist[position-1]>currentvalue:alist[position]=alist[position-1]position=position-1alist[position]=currentvalue

(五)谢尔排序算法 (Shell Sort)

​ 我们注意到,插入排序的比对次数,在最好的情况下是O(n),这种情况是发生在列表已是有序的情况下,实际上,列表越接近有序,插入排序的比对次数就越少。从这种情况入手,谢尔排序以插入排序作为基础,对无序表进行间隔划分子列表,每个子列表都执行插入排序,同时间隔越来越小,最后一次进行的是标准的插入排序,不过由于这时列表的有序性已经很强了,所以需要进行的操作很少。至于该算法具体的时间复杂度,证明比较复杂故不给出具体证明,不过它的时间复杂度是在O(n)与O(n²)之间的。

def shellSort(alist):sublistCount=len(alist)//2while sublistCount>0:for startposition in range(sublistCount):gapInsertionSort(alist,startposition,sublistCount)sublistCount=sublistCount//2def gapInsertionSort(alist,start,gap):for i in range(start,len(alist),gap):currentvalue=alist[i]position=iwhile position>=gap and alist[position-gap]>currentvalue:alist[position]=alist[position-gap]position=position-gapalist[position]=currentvalue

(六)归并排序算法 (Merge Sort)

​ 归并排序是递归算法,是分治策略在排序中的应用,思路是将数据表持续分裂为两半,对两半分别进行归并排序。

#1 常规版
def mergeSort(alist):if len(alist)>1:mid=len(alist)//2lefthalf=alist[:mid]righthalf=alist[mid:]mergeSort(lefthalf)mergeSort(righthalf)i=j=k=0while i<len(lefthalf) and j<len(righthalf):if lefthalf[i]<righthalf[j]:alist[k]=lefthalf[i]i+=1else:alist[k]=righthalf[j]j+=1k+=1while i<len(lefthalf):alist[k]=lefthalf[i]i+=1k+=1while j<len(righthalf):alist[k]=right[j]j+=1k+=1#2 更pythonic的版本
def mergeSort(alist):if len(alist)<=1:return alistmid=len(alist)//2left=mergeSort(alist[:mid])right=mergeSort(alist[mid:])merged=[]while left and right:if left[0]<right[0]:merged.append(left.pop(0))else:merged.append(right.pop(0))merged.extend(right if right else left)return merged
  • 将归并排序分为两个过程来分析:分裂和归并。分裂的过程,借鉴二分查找法中的分析结果,是对数复杂度,即时间复杂度为O(logn),归并的过程,相对于分裂的每一个部分,其所有的数据项都会被比较和放置一次,,所以是线性复杂度,即时间复杂度为O(n)。综合考虑,每次分裂的部分都进行一次O(n)的数据项归并,总的时间复杂度为O(nlogn)。
  • 还是要注意到两个切片操作,为了时间复杂度精确起见,可以通过取消切片操作,改为传递两个分裂部分的起始点和终止点,也是可的,只是算法可读性会牺牲一点点。
  • 此外,归并排序算法用到了额外一倍的存储空间用于归并,这一点在对特大数据集进行排序的时候要考虑进去。

(七)快速排序算法 (Quick Sort)

​ 快速排序算法也是一种递归算法,其思路是依据一个“中值”(最基础的做法是直接选取第一个数据)数据项来把数据表分为两半:小于中值的一半和大于中值的一半,然后让每部分分别进行快速排序(递归)。

def quickSort(alist):quickSortHelper(alist,0,len(alist)-1)
def quickSortHelper(alist,first,last):if first<last:  #基本结束条件splitpoint=partition(alist,first,last)#递归调用,不断进行二分处理quickSortHelper(alist,first,splitpoint-1)quickSortHelper(alist,splitpoint+1,last)
def partition(alist,first,last):pivotvalue=alist[first] #选取第一个值为“中值”(即切分点)leftmark=first+1rightmark=lastdone=Falsewhile not done:while leftmark<=rightmark and alist[leftmark]<=pivotvalue:leftmark=leftmark+1while leftmark<=rightmark and alist[rightmark]>=pivotvalue:rightmark=rightmark-1#若走出了上述两个循环,则必然是破坏了其中一个条件if right<leftmark: #如果破坏的是条件1done=Trueelse:  #如果破坏的是条件2和3temp=alist[leftmark]alist[leftmark]=alist[rightmark]alist[rightmark]=temp#排序完毕后,将“中值”点移到它该在的位置temp0=alist[first]alist[first]=alist[rightmark]alist[rightmark]=temp0return rightmark
  • 快速排序过程分为两部分:分裂和移动,如果分裂总能把数据表分为相等的两部分,那么就是O(logn)的复杂度,而移动需要将每项都与中值项对比,还是O(n),综合起来就是O(nlogn),而且算法运行过程中不需要额外的存储空间。
  • 但是,如果不那么幸运的话,中值所在的分裂点过于偏离中部,造成左右两部分数量不平衡;极端情况,有一部分始终没有数据,这样时间复杂度就会退化到O(n²),还要加上递归造成的开销,效果比冒泡排序还要糟糕。若对其进行改进,可以适当改进中值的选取方法,让中值更具有代表性。

(八)散列表(哈希表 Hash Table)

  • 实现从数据项到存储槽名称的转换的,称为散列函数(hash function),散列函数有很多方式,比如取余数以及相关的很多变体,然而它有可能会产生一些冲突(collision),如果不会产生冲突,那么该函数成为完美散列函数,但如果数据项经常性地变动,则很难有一个系统性的方法设计对应的完美散列函数,因此,近似完美或是使用一些处理冲突的方法就可以啦。最著名的近似完美散列函数是MD5和SHA系列函数,它们虽然能够以极特殊的情况来构造个别碰撞(散列冲突),但在实用中从未有实际的威胁。

  • Python自带MD5和SHA系列的散列函数库:hashlib ,它包括了md5 / sha1 / sha224 / sha256 / sha384 / sha512 等六种散列函数。

    import hashlib
    #用法1: 对单个字符进行散列计算
    hashlib.md5("hello world").hexdigest()
    hashlib.sha1("hello world").hexdigest()
    #用法2: 用update方法对任意长的数据分部分来计算(这样不管多大的数据都不会有内存不足的问题)
    m=hashlib.md5()
    m.update("hello world")
    m.update("this is part #2")
    m.update("this is part #3")
    m.hexdigest()
    
  • 散列函数设计:折叠法、折叠法(隔数反转等微调变体)、平方取中法,非数项只需把字符串中每个字符看作ASCⅡ码即可,然后再使用各种设计方法,如以下是一个对其ASCⅡ编码各自相加求和并取余的简单版本:

def hash(astring,tablesize):sum=0for pos in range(len(astring)):sum+=ord(astring[pos])return sum%tablesize
  • 冲突解决方案:

    1.开放定址(opening adressing) 线性探测(linear probing)

    ​ 重新寻找空槽的过程可以用一个更通用的“再散列(rehashing)”来概括

    #线性探测
    rehash(pos)=(pos+1)%sizeoftable
    #跳跃式探测
    rehash(pos)=(pos+skip)%sizeoftable
    #二次探测(quadratic probing)
    #略
    

    2.数据项链(Chaining)

    ​ 将容纳单个数据项的槽扩展为容纳数据项集合(或者对数据项链表的引用),一般就只能顺序查找了,随着散列冲突的增加,数据项的查找时间也会增加。

(九)映射抽象数据类型及Python实现(使用散列表实现)

​ 例如python最有用的数据类型之一“字典”,是一种可以保存key-data键值对的数据类型,这种键值关联的方法称为“映射Map”,ADT Map的结构是键-值关联的无序集合,其关键码具有唯一性,通过关键码可以唯一确定一个数据值。

​ ADT Map定义的操作如下:

map=Map() #创建一个新映射,返回空映射对象
map.put(key,val) #将新关联加入映射中,如果key已存在,则将val替换旧关联值
map.get(key) #给定ky,返回关联的数据值,如不存在,则返回None
del map[key] #删除该ket-val关联
len(map) #返回映射中key-val关联的数目
key in map #返回key是否存在于关联中(布尔值)

​ 用字典的优势在于,给定关键码key,能够很快得到关联的数据值data。为了达到快速查找的目标,需要一个支持高效查找的ADT实现,可以采用列表数据结构加顺序查找或者二分查找,当然,更为合适的是使用前述的散列表来实现,这样可以达到最快O(1)的性能。

​ 下面,我们用一个HashTable类来实现ADT Map,该类包含了两个列表作为成员,其中一个slot列表用于保存key,另一个平行的data列表用于保存数据项。在slot列表查找到一个key的位置以后,在data列表对应相同位置的数据项即为关联数据。

class HashTable:def __init__(self):self.size=11 #创建其容量为11(一般选择素数会更好一点)self.slots=[None]*self.size #创建一个保存key的列表self.data=[None]*self.size #创建一个保存data的列表#定义一个进行转换的哈希函数def hashfunction(self,key): return key%self.size#定义一个进行线性探测的函数(此处的skip选择为1)def rehash(self,key): return (oldhash+1)%self.size# put方法def put(self,key,data):hashvalue=hashfuction(key) #先利用哈希函数进行转换(可以理解为编码解码那种)#若该处为空,就直接放进去了if self.slots[hashvalue]==None:self.slots[hashvalue]=keyself.data[hashvalue]=data#若该处不为空else:#如果对应的就是这个key值,那么就replaceif self.slots[hashvalue]==key:self.data[hashvalue]=data#如果不是,就进行线性探测else:nextslot=self.rehash(hashvalue)#只要没有找到空位或者找到一个对应key的地方,就一直延伸下去进行探测while self.slots[nextslot]!=None and self.slots[nextslot]!=key:nextslot=self.rehash(nextslot)#退出循环的原因若是 “找到空位”if self.slots[nextslot]==None:self.slots[nextslot]=keyself.data[nextslot]=data#退出循环的原因若是 “找到了一个对应key的位置”else:self.data[nextslot]=data #replace# get方法def get(self,key):startslot=self.hashfuction(key) #通过哈希函数得到key对应的编码data=Nonestop=Falsefound=Falseposition=startslotwhile self.slots[position]!=None and not found and not stop:#如果非常顺利地直接得到,那么就找到啦if self.slots[position]==key:found=Truedata=self.data[position]#如果没有,那么就考虑,进行线性探测,若绕了一圈回到原位还没找到就说明没有else:position=self.rehash(position) #进行线性探测,然后回到while循环开头进行if判断#如果绕了一圈回到原位,则说明没有,可停止,返回最初的Noneif position==startslot:stop=Truereturn data#通过特殊方法实现访问def __getitem__(self,key):return self.get(key)def __setitem__(self,key,data):self.put(key,data)

五.树及算法

(一)树的实现

1.嵌套链表实现
def BinaryTree(r):return [r,[],[]]def insertLeft(root,newBranch):t=root.pop(1)if len(t)>1:root.insert(1,[newBranch,t,[]])else:root.insert(1,[newBranch,[],[]])return rootdef insertRight(root,newBranch):t=root.pop(2)if len(t)>1:root.insert(2,[newBranch,[],t])else:root.insert(2,[newBranch,[],[]])return rootdef getRootVal(root):return root[0]def setRootVal(root):root[0]=newvaldef getLeftChild(root):return root[1]def getRightChild(root):return root[2]
2.链表实现
class BinaryTree:def __init__(self,rootObj):self.key=rootObjself.leftChild=Noneself.rightChild=Nonedef insertLeft(self,newNode):if self.leftChild==None:self.leftChild=newNodeelse:t=BinaryTree(newNode)t.leftChild=self.leftChildself.leftChild=tdef insertRight(self,newNode):if self.rightChild==None:self.rightChild=newNodeelse:t=BinaryTree(newNode)t.rightChild=self.rightChildself.rightChild=tdef getRootVal(self):return self.keydef setRootVal(self,obj):self.key=objdef getLeftChild(self):return self.leftChilddef getRightChild(self):return self.rightChild

(二)树的应用 —— 表达式解析

1. 建立表达式解析树
def buildParseTree(fpexp):fplist=fpexp.split()pstack=Stack()eTree=BinaryTree('')pstack.push(eTree)currentTree=eTreefor i in fplist:if i=='(':currentTree.insertLeft('')pstack.push(currentTree)currentTree=currentTree.getLeftChild()elif i not in ['+','-','*','/',')']:currentTree.setRootVal(int(i))currentTree=pstack.pop()elif i in ['+','-','*','/']:currentTree.setRootVal(i)currentTree.insertRight('')pstack.push(currentTree)currentTree=currentTree.getRightChild()elif i==')':currentTree=pstack.pop()else:raise ValueErrorreturn eTree
2. 利用表达式解析树求值

​ 由于二叉树是一个递归数据结构,自然可以用递归算法来处理

import operator #这样其实是增强了程序的可读性
def evaluate(parseTree):opers={'+':operator.add,'-':operator.sub,'*':operator.mul,'/':operator.truediv}leftC=parseTree.getLeftChild()rightC=parseTree.getRightChild()if leftC and rightC:fn=opers[parseTree.getRootVal()]return fn(evaluate(leftC),evaluate(rightC))else:return parseTree.getRootVal()

(三)树的遍历 (Tree Traversals)

1. 树的遍历:递归算法代码
1.1 直接定义函数实现
#1 前序遍历(preorder)【中、左、右】
def preorder(tree):if tree:print(tree.getRootVal())preorder(tree.getLeftChild())preorder(tree.getRightChild())#2 中序遍历(inorder)【左、中、右】
def inorder(tree):if tree:inorder(tree.getLeftChild())print(tree.getRootVal())inorder(tree.getRightChild())#3 后序遍历(postorder)【左、右、中】
def postorder(tree):if tree:postorder(tree.getLeftChild())postorder(tree.getRightChild())print(tree.getRootVal())
1.2 在BinaryTree类中实现
def preorder(self):print(self.key)if self.leftChild:self.leftChild.preorder()if self.rightChild:self.rightChild.preorder()
# inorder/postorder同理类似,此处略
2.后序遍历:表达式求值

​ 前面我们已经提出了一种递归方式(其本质也是…广义上的后序遍历叭)来进行表达式求值,这里我们再利用更明确的后序遍历进行求值。

#1 先贴上之前那种方法
import operator
def evaluate(parseTree):opers={'+':operator.add,'-':operator.sub,'*':operator.mul,'/':operator.truediv}leftC=parseTree.getLeftChild()rightC=parseTree.getRightChild()if leftC and rightC:fn=opers[parseTree.getRootVal()]return fn(evaluate(leftC),evaluate(rightC))else:return parseTree.getRootVal()#2 新方法
import operator
def postordereval(parseTree):opers={'+':operator.add,'-':operator.sub,'*':operator.mul,'/':operator.truediv}res1=Noneres2=Noneif tree:res1=postordereval(parseTree.getLeftChild())res2=postordereval(parseTree.getRightChild())if res1 and res2:return opers[parseTree.getRootVal()](res1,res2)else:return parseTree.getRootVal()#总结一下这俩的区别,我觉得是:第一个它更强调对左右子树的函数操作结果进行四则运算,强调函数符最后,但左右之间应该是没有非常明确的先后次序,就…看着弄嘛…我感觉应该是存在那么一点异步性的感觉?第二个就非常明确,每当面临抉择都是优先左再右再中,就是步序划分的非常非常明晰……应该是这样叭…
3.中序遍历:生成全括号中缀表达式
def printexp(tree):sVal=''if tree:sVal='('+printexp(tree.getLeftChild())sVal=sVal+str(tree.getRootVal())sVal=sVal+printexp(tree.getRightChild()+')')return sVal

(四)优先队列和二叉堆

​ 实现优先队列的经典方案是采用二叉堆数据结构,二叉堆的有趣之处在于,其逻辑结构上像二叉树,但却是用非嵌套的列表实现的。我们采用“完全二叉树”的结构来实现平衡,完全二叉树由于其特殊性,可以用非嵌套列表实现。然后对于每一条路径,它都是有顺序的(堆次序 Heap Order)。

  1. ADT BinaryHeap的操作定义如下:
bh=BinaryHeap() #创建一个空二叉堆对象
bh.insert(k) #将新key加入到堆中
bh.findMin() #返回堆的最小项,最小项仍保留
bh.delMin() #返回堆的最小项,同时从堆中删除
bh.isEmpty() #返回堆是否为空
bh.size() #返回堆中key的个数
bh.bulidHeap(list) #从一个key列表创建新堆
  1. 二叉堆操作的实现
class BinHeap:#1 初始化方法def __init__(self):self.heapList=[0]self.currentSize=0#2 插入新元素的操作def percUp(self,i): # 沿路径向上,与父节点交换【上升】while i//2>0:if self.heapList[i]>self.heapList[i//2]:tmp=self.heapList[i]self.heapList[i]=self.heaoList[i//2]self.heapList[i//2]=tempi=i//2def insert(self,k):self.heapList.append(k)self.currentSize+=1self.percUp(self.currentSize) #调用【上升】操作的函数#3 移除最小者的操作def minChild(self,i): #确认并获取两个子节点中更小的那个节点if i*2+1>self.currentSize: #唯一子节点return i*2else:                      #两侧都有的正常情况if self.heapList[i*2]<self.heapList[i*2+1]:return i*2else:return i*2+1def percDown(self,i):  #沿路径向下,与子节点交换【下沉】while (i*2)<=self.currentSize:mc=self.minChild(i)if self.heapList[i]>self.heapList[mc]:tmp=self.heapList[i]self.heapList[i]=self.heapList[mc]self.heapList[mc]=tmpi=mcdef delMin(self):retval=self.heapList[1]self.heapList[1]=self.heapList[self.currentSize]self.currentSize=self.currentSize-1self.heapList.pop()self.percDown(1) #调用【下沉】操作的函数return retval#4 从无序表生成堆的操作 →能将总代价控制在O(n)def buildHeap(self,alist):self.currentSize=len(alist)self.heapList=[0]+alist[:]i=len(alist)//2 #只需从最后节点的父节点开始,因为叶节点无需下沉while i>0:self.percDown(i)i=i-1print(self.heapList,i)

(五)二叉查找树(BST)及其基本操作

​ 二叉查找树BST的性质:比父节点小的key都出现在左子树,比父节点大的key都出现在右子树(对于每一个节点都是如此)。

​ 二叉查找树的实现:节点和链接结构,需要用到BST和TreeNode两个类,BST的root成员引用根节点TreeNode

# BST
class BinarySearchTree:def __init__(self):self.root=Noneself.size=0def length(self):return self.sizedef __len__(self):return self.size def __iter__(self):return self.root.__iter__()# TreeNode
class TreeNode:def __init__(self,key,val,left=None,right=None,parent=None):self.key=keyself.payload=valself.leftChild=leftself.rightChild=rightself.parent=parentdef hasLeftChild(self):return self.leftChilddef hasRightChild(self):return self.rightChilddef isLeftChild(self):return self.parent and self.parent.leftChild==selfdef isRightChild(self):return self.parent and self.parent.rightChild==selfdef isRoot(self):return not self.parentdef isLeaf(self):return not (self.rightChild or self.leftChild)def hasAnyChild(self):return self.leftChild or self.rightChilddef hasBothChild(self):return self.leftChild and self.rightChilddef replaceNodeData(self,key,value,lc,rc):self.key=keyself.payload=valueself.leftChild=lcself.rightChild=rcif self.hasLeftChild():self.leftChild.parent=selfif self.hasRightChild():self.rightChild.parent=self

(六)二叉查找树实现及算法分析

#声明1:这些都是定义在BST类下的方法嗷#1 构建二叉树 加入
def put(self,key,val):if self.root:self._put(key,val,self.root)else:self.root=TreeNode(key,val)self.size+=1def _put(self,key,val,currentNode):if key<currentNode.key:if currentNode.hasLeftChild():self._put(key.val,currentNode.leftChild)else:currentNode.leftChild=TreeNode(key,val,parent=currentNode)else:if currentNode.hasRightChild():self._put(key,val,currentNode.rightChild)else:currentNode.rightChild=TreeNode(key,val,parent=currentNode)#顺便把 __setitem__ (那个像字典加项一样的更简洁的方法)做了
def __setitem__(self,k,v):self.put(k,v)#2 BST.get方法
def get(self,key):if self.root:res=self._get(key,self.root) #在这里调用了一个能够自己递归着探索完每一个结点的函数#(我觉得是因为那个函数要递归,如果不自己写成一个函数没法递归)if res:return res.payloadelse:return Noneelse:return Nonedef _get(self,key,currentNode):if not currentNode:return Noneelif currentNode.key==key:return currentNodeelif key<currentNode.key:return self._get(key,currentNode.leftChild) #递归else:return self._get(key,currentNode,rightChild) #递归def __getitem__(self,key):   #实现val=myziptree['thekey']return self.get(key)
def __contains__(self,key):if self._get(key,self.root):return Trueelse:return Falsedef __iter__(self):   #实现for i in myTree:这种遍历,是一个迭代器函数(此处使用的是中序遍历)if self:if self.hasLeftChild():for elem in self.leftChild: #这里应该理解为是调用自己,因此是迭代函数yield elem  #左yield self.key      #中if self.hasRightChild():for elem in self.rightChild:yield elem  #右#BST.delete方法 (最复杂的,先用_get找到要删除的节点,再用remove移除)
def delete(self,key):if self.size>1:nodeToRemove=self._get(key,self.root)if nodeToRemove:self.remove(nodeToRemove)self.size=self.size-1else:raise KeyError('Error,key not in tree')elif self.size==1 and self.root.key=key:self.root=Noneself.size=self.size-1else:raise KeyError('Error,key not in tree')def __delitem__(self,key):self.delete(key)def remove(self,currentNode):#第一种情形:该节点为叶节点if currentNode.isLeaf():if currentNode==currentNode.parent.leftChild:currentNode.parent.leftChild=Noneelse:currentNode.parent.rightChild=None#第二种情形:该节点有且仅有一个子节点else:if currentNode.hasLeftChild():if currentNode.isLeftChild():currentNode.leftChild.parent=currentNode.parentcurrentNode.parent.leftChild=currentNode.leftChildelif currentNode.isRightChild():currentNode.rightChild.parent=currentNode.parentcurrentNode.parent.leftChild=currentNode.leftChildelse:currentNode.replaceNodeData(currentNode.leftChild.key,currentNode.leftChild.payload,currentNode.leftChild.leftChild,currentNode.leftChild.rightChild)else:if currentNode.isRightChild():currentNode.rightChild.parent=currentNode.parentcurrentNode.parent.rightChild=currentNode.rightChildelif currentNode.isLeftChild():currentNode.rightChild.parent=currentNode.parentcurrentNode.parent.leftChild=currentNode.rightChildelse:currentNode.replaceNodeData(currentNode.rightChild.key,currentNode.rightChild.payload,currentNode.rightChild.leftChild,currentNode.rightChild.rightChild)#第三种情形:该节点拥有两个子节点 (如果选择把其中的某一个子节点移上来,就会造成新的空缺,就会带来一连串的移动,复杂度很高;因此选择找到一个合适的节点(应当是一个叶节点)直接进行一次替换,这个节点就是它的“后继节点”,即大于该节点的最小节点。)elif currentNode.hasBothChildren():#接下来两句是调用了将要对TreeNode类设定的方法succ=currentNode.findSuccessor() succ.spliceOut()currentNode.key=succ.keycurrentNode.payload=succ.payload#声明2 以下是对于TreeNode类设定的方法#1 寻找后继节点
def findSuccessor(self):succ=Noneif self.hasRightChild():succ=self.rightChild.findMin()#[]这一块在这个算法里用不到else:if self.parent:if self.isLeftChild():succ=self.parent #左侧连环移相当于直接删掉左侧正数第二个相当于用parent代替它else:self.parent.rightChild=Nonesucc=self.parent.findSuccessor()#这个…就相当于去左边找?之类的…self.parent.rightChild=self#[\]return succ
def findMin(self): #即“到左下角”current=selfwhile current.hasLeftChild():current=current.leftChildreturn current#2 摘出节点
def spliceOut(self):if self.isLeaf():if self.isLeftChild():self.parent.leftChild=Noneelse:self.parent.rightChild=Noneelif self.hasAnyChildren():if self.hasLeftChild(): #在这个题中是不可能的,但是为了逻辑的完整性,还是写上if self.isLeftChild():self.parent.leftChild=self.leftChildelse:self.parent.rightChild=self.rightChildelse:if self.isLeftChild():self.parent.leftChild=self.leftChildelse:self.parent.rightChild=self.rightChildself.rightChild.parent=self.parent

​ 二叉查找树算法的性能取决于二叉搜索树的高度(最大层次),而其高度又受数据项key插入顺序的影响,如果key列表是随机分配的话,那么大于和小于key的键值数量大致相等,BST的高度就是log2(n),则put方法的最差性能为O(logn);但若key列表分布是极端情况,(如从小到大插入),那么put方法的性能就会变成O(n)。

(七)AVL树

#1 重新定义_put方法
def _put(self,key,val,currentNode):if key<currentNode.key: #如果小于(则应该插入到左子树)if currentNode.hasLeftChild():#如果已有左子树,就递归着继续往下走直到没有左子树了再进行创建self._put(key,val,currentNode.leftChild) else: #若没有左子树,直接创建一个左子节点currentNode.leftChild=TreeNode(key,val,parrent=currentNode)self.updateBalance(currentNode.leftChild) #进行了创建之后,就需要重新调整平衡else: #如果大于(则应该插入到右子树)if currentNode.hasRightChild():self._put(key,val,currentNode.rightChild)else:currentNode.rightChild=TreeNode(key,val,parent=currentNode)self.updateBalance(currentNode.rightChild)#2 定义一个在里面调用的“调整平衡”的函数
def updateBalance(self,node):#倘若这个Node处不平衡了,那就必须进行调用rebalance方法的大修if node.balanceFactor>1 or node.balanceFactor<-1:self.rebalance(node) #【新函数调用】returnif node.parent!=None:#作为左右节点对于factor的影响,相当于是一个factor的计算过程,从它末端开始,倘若它不为0,就往上传递影响#它对于父节点的影响if node.isLeftChild():node.parent.balanceFactor+=1elif node.isRightChild():node.parent.banlanceFactor-=1#判断父节点的factor是否为0,如果不是,它就要对父节点的父节点造成影响了【递归调用】if node.parent.balanceFactor!=0:self.updateBalance(node.parent)#3 定义一个进行“大修”的函数
def rebalance(self,node):#右重则左旋if node.balanceFactor<0: #内层循环是为了防止那种,如果右重但是左旋之后会左重这种无解的情况,就要对子节点也进行一个旋转,就可以较好解决if node.rightChild.balanceFactor>0:self.rotateRight(node.rightChild)else:self.rotateLeft(node)#左重则右旋elif node.balanceFactor>0: #内层循环是为了防止那种,如果右重但是左旋之后会左重这种无解的情况,就要对子节点也进行一个旋转,就可以较好解决if node.leftChild.balanceFactor<0:self.rotateLeft(node.leftChild)else:self.rotateRight(node)#4 定义函数进行左旋和右旋
def rotateLeft(self,rotRoot):newRoot=rotRoot.rightChild#part1 如果newRoot本身有左子节点,那么rotRoot就从它的父节点的位置拿下来放到newRoot的左子节点的位置,然后newRoot那个位置上原本的左子节点就要放到rotRoot的右子节点处(也相当于代替newRoot原来相对于rtRoot的位置)【rotRoot去抢newRoot左子节点的位置,并让它这个左子节点替父从军代替它原本在的位置】rotRoot.rightChild=newRoot.leftChildif newRoot.leftChild!=None:newRoot.leftChild.parent=rotRoot#part2 修改newRoot的根节点    #向上的指针newRoot.parent=rotRoot.parent#向下的指针if rotRoot.isRoot():self.root=newRootelse:if rotRoot.isLeftChild():rotRoot.parent.leftChild=newRootelse:rotRoot.parent.rightChild=newRoot#part3 修改newRoot的子节点newRoot.leftChild=rotRoot #向下指针rotRoot.parent=newRoot #向上指针#至于那个可能的“替代理论”已经在part1讨论过了(不过那只是一种对于“如果”的解决方案)#part4 修改两个节点的factorrotRoot.balanceFactor+=1-min(newRoot.balanceFactor,0)newRoot.balanceFactor-=1+max(rotRoot.balanceFactor,0)

​ put方法分为两个部分:一部分是put,另一部分是若引发了不平衡则进行重新平衡。put部分的插入和更新代价最多为O(logn),而如果引发了不平衡,那么重新平衡最多需要2次旋转,时间复杂度为O(1);综上所述,整个put方法的时间复杂度还是O(logn),始终维持平衡,保持着高性能,相比之前普通的BST确实有了优化。

六. 图和算法

(一)图抽象数据类型及其Python实现

1.基本方法
graph=Graph() #创建一个空的图
graph.addVertex(vert) #将顶点vert加入图中
graph.addEdge(fromVert,toVert) #添加有向边
graph.addEdge(fromVert,toVert,weight) #添加带权的有向边
graph.getVertex(vKey) #查找名称为vkey的顶点
graph.getVertices() #返回图中所有顶点列表
vert in graph #返回顶点是否存在图中True/False

ADT Graph的实现方法有两种主要形式:邻接矩阵、邻接表,两种方式各有优劣,需要在不同应用中加以选择。

2.实现

​ 我觉得是用的邻接表实现。

#1 顶点
class Vertex:def __init__(self,key): #初始化方法,给节点名字并给它一个空字典来存储它所相连接的节点self.id=keyself.connectedTo={} def addNeighbor(self,nbr,weight=0): #增加邻接节点self.connecterTo[nbr]=weightdef __str__(self):return str(self.id)+'connectedTo:'+str([x.id for x in self.connectedTo])def getConnections(self): #获得它所拥有的所有节点列表return self.connectionTo.keys()def getId(self): #获得它的idreturn self.iddef getWeight(self,nbr): #获得某个连接的权重return self.connectedTo[nbr]#2 图Graph
class Graph:def __init__(self): #初始化方法,创建一个空字典用来存储所有的节点,创建一个变量用来计数self.vertList={}self.numVertices=0def addVertex(self,key): #加入新的节点(实例化一个节点类,然后把它与相应的键以键值对的形式存储到节点字典中)self.numVertices+=1newVertex=Vertex(key)self.vertList[key]=newVertexreturn newVertexdef getVertex(self,k): #得到某个节点if k in self.vertList:return self.vertList[k]def __contains__(self,n): #使用x in xxx 这种返回True or Falsereturn n in self.vertListdef addEdge(self,f,t,cost=0): #添加新的边(如果里面有的节点还没有,就先调用方法创建)if f not in self.vertList:nv=self.addVertex(f)if t not in self.vertList:nv=self.addVertex(t)self.vertList[f].addNeighbor(self.vertList[t],cost)def getVertices(self): #得到所有的节点列表return self.vertList.keys()def __iter__(self): #使用迭代方法return iter(self.vertList.values())

(二)图的应用:词梯问题

1.方法一 :构建bucket
def buildGraph(wordfile):d={} #创建一个字典,用来存储不同的bucketg=Graph() #创建一个图wfile=open(wordfile,'r') #读取包含相应单词的文档#创建bucket并将word写入bucketfor line in wfile:word=line[:-1] #取到word#构建bucket并写入for i in range(len(word)): #获得bucketbucket=word[:i]+'_'+word[i+1:]if bucket in d:d[bucket].append(word) #写入else:d[bucket]=[word] #创建+写入#完成图,即将得到的buckets,每个里面的words之间建立图的边for bucket in d.keys(): #对于每一个bucket#每两个单词之间形成边,最终构成图for word1 in d[bucket]: for word2 in d[bucket]:if word1!=word2:g.addEdge(word1,word2)return g
2.方法二 :广度优先搜索(Breadth First Search,BFS)

​ BFS是搜索图的最简单算法之一,也是其它一些重要的图算法的基础。

#1 广度优先搜索整理图
def bfs(g,start):start.setDistance(0) #将初始节点距离设为0start.setPred(None) #将初始节点的父节点设置为NoneverQueue=Queue() #创建一个空队列,用来装等待操作的节点(此处,操作是指“搜索其所有的邻接节点,并判断颜色,黑色略过,白色则变灰、设置距离并加入队列末端(其实也就相当于开启下一行啦,但是不重要啊,顺序读取不就是这样吗)”)vertQueue.enqueue(start) #把初始节点加入队列while (vertQueue.size()>0):currentVert=vertQueue.dequeue() #顺序从队列中读取灰色节点,完成其nbr的全部处理后设置为黑色for nbr in currentVert.getConnections(): #处理该灰色节点的所有白色nbrif (nbr.getColor()=='white'):nbr.setColor('gray') #设置为灰色nbr.setDistance(currentVert.getDistance()+1) #设置距离nbr.setPred(currentVert) #设置父节点vertQueue.enqueue(nbr) #将这个新生成的灰色节点加入队尾currentVert.setColor('black')#在以初始单词为起始顶点,遍历了所有顶点并为每个顶点着色、赋距离和前驱之后(即通过广度优先搜索整理完这个图之后),就可以通过一个回途追溯函数来确定初始单词到任何单词顶点的最短词梯。            #2 回途追溯函数
def traverse(y):x=ywhile (x.getPred()):print(x.getId())x=x.getPred()print(x.getId())

算法分析:

  • BFS算法主体是两个循环的嵌套:while循环对每个顶点访问一次,所以是O(|V|),而嵌套在里面的for循环,由于每条边只有在其起始顶点u出队的时候才会被检查一次,而每个顶点最多出队1次,所以每个边最多被检查一次,一共是O(|E|);综上,BFS算法的时间复杂度为O(|V|+|E|)。(这个理解要注意,你可以理解为,每次进行搜寻顶点,虽然是在循环内进行搜索边的工作,但是是每次它们所搜索的边的和|E|,所以对叭。)

  • 回途追溯函数的最坏时间复杂度为O(|V|)。

  • 除此以外,针对这个问题,也就是词梯问题,本身构建这个图所需要的时间复杂度是O(|V|²) 。

(三)图的应用:骑士周游世界问题

1.图的构建
#1 构建图的主体函数
def knightGraph(bdSize):ktGraph=Graph() #建一个空图#双重循环,也就代表形成了所有的棋盘格for row in range(bdSize):for col in range(bdSize):nodeId=posToNodeId(row,col,bdSize) #对于每一个格子都标注好其特定的nodeIdnewPositions=genLegalMoves(row,col,bdSize) #得到其所有的合法下一步,封装在列表里for e in newPositions: #对于每一个合法的下一步进行操作nid=posToNodeId(e[0],e[1],bdSize) #得到本次循环中的这个合法下一步ktGraph.addEdge(nodeId,nid) #连接边return ktGraph#2 计算每一个格子id的小函数
def posToNodeId(row,col,bdSize): return row*bdSize+col#3 计算每一个格子对应的合法下一步的函数
def genLegalMoves(x,y,bdSizes):newMoves=[] #创建一个空列表用于装所有的合法下一步moveOffsets=[(-1,-2),(1,-2),(-1,2),(1,2),(-2,-1),(2,-1),(-2,1),(2,1)]  # “												

数据结构与算法(Python)【PKU MOOC】相关推荐

  1. 数据结构与算法python版 MOOC 第九周

    九.树及算法-上 本系列博客基于" (北京大学)数据结构与算法python版"慕课,课程在中国大学慕课和bilibili上均可找到. 1. 内容 树结构的相关术语 树的表示方法:嵌 ...

  2. 数据结构与算法python版 MOOC 第三周

    三.基本线性结构 本系列博客基于" (北京大学)数据结构与算法python版"慕课,课程在中国大学慕课和bilibili上均可找到. 1. 内容 定义线性结构 讲解栈的结构结构 栈 ...

  3. 数据结构与算法Python版MOOC笔记及练习【七】

    文章目录 什么是顺序查找 算法分析 二分查找 冒泡算法思路 插入排序 谢尔排序 归并排序 快速排序 课程练习 什么是顺序查找 数据项保存在像列表这样的集合中,我们会称这些数据项具有线性或者顺序关系. ...

  4. mooc数据结构与算法python版期末测验_中国大学MOOC(慕课)_数据结构与算法Python版_测试题及答案...

    中国大学MOOC(慕课)_数据结构与算法Python版_测试题及答案 更多相关问题 采用fopen()函数打开文件,支持文件读取的参数有: [简答题]简单阐述高分子材料热-机械特征及成型加工的关系,并 ...

  5. mooc数据结构与算法python版期末考试_数据结构与算法Python版-中国大学mooc-试题题目及答案...

    数据结构与算法Python版-中国大学mooc-试题题目及答案 更多相关问题 婴儿出生一两天后就有笑的反应,这种笑的反应属于(). [判断题]填制原始凭证,汉字大写金额数字一律用正楷或草书书写,汉字大 ...

  6. mooc数据结构与算法python版期末测验_中国大学数据结构与算法Python版答案_MOOC慕课章节期末答案...

    中国大学数据结构与算法Python版答案_MOOC慕课章节期末答案 更多相关问题 java.lang 包的 Character 类的 isJavaIdentifierStart 方法的功能是用来判断某 ...

  7. mooc数据结构与算法python版第十一周作业_中国大学 MOOC_数据结构与算法Python版_2020最新答案学习指南...

    中国大学 MOOC_数据结构与算法Python版_2020最新答案学习指南 更多相关问题 [判断题]实际集成运放的上限截止频率为无穷大 [多选题]现代城市的发展凸现出与以往不同的动力机制包括 教师在引 ...

  8. PDF课件下载!北京大学公开课《数据结构与算法Python版》

    北京大学公开课<数据结构与算法Python版>,面向具有Python语言程序设计基础的大学生和社会公众,介绍常见的基本数据结构以及相关经典算法,强调问题-数据-算法的抽象过程,关注数据结构 ...

  9. python数据结构算法 北京大学_北京大学公开课《数据结构与算法Python版》

    之前我分享过一个数据结构与算法的课程,很多小伙伴私信我问有没有Python版. 看了一些公开课后,今天特向大家推荐北京大学的这门课程:<数据结构与算法Python版>. 课程概述 很多同学 ...

  10. 数据结构与算法python描述_数据结构与算法——Python语言描述.pdf

    数据结构与算法--Python语言描述.pdf 欢迎加入非盈利Python编学习交流程QQ群783462347,群里免费提供500+本Python书籍! 欢迎加入非盈利Python编程学习交流程QQ群 ...

最新文章

  1. 配置ssh公钥登录提示还是输入密码
  2. 归根到底,大家都是出来卖的!有的人月入十万,有的人月入三千!差距!
  3. Ubuntu10下MySQL搭建Amoeba_读写分离
  4. linux curl 命令 http请求、下载文件、ftp上传下载
  5. [ 1001] 动态开辟二维数组的说明
  6. Discuz1.5 密码错误次数过多,请 15 分钟后重新登录
  7. 有效Java第三版的源代码已更新为使用较新的功能
  8. 设计模式(一)单例模式:5-单元素枚举类模式
  9. 数据库异常---ORA-01436: 用户数据中的 CONNECT BY loop in user data 循环
  10. git 学习指南 学习资料笔记
  11. 公历转农历C语言课程设计,(只为学习)公历转农历代码以完成,请高手在此代码基础上写出个农历转公历的代码出来...
  12. 1、当鼠标移动到目标上的时候,自动显示一个提示框。
  13. HTML5实现3D校园地图思路
  14. Excel的Sumif函数
  15. WEB端支付宝接入----统一收单下单并支付页面接口
  16. oracle导出辅助账明细,AO2011导入国库集中支付系统3.0的辅助账
  17. 3U8633——雷达对民航的贡献
  18. 【Linux 内核 内存管理】分区伙伴分配器 ② ( free_area 空闲区域结构体源码 | 分配标志位 | GFP_ZONE_TABLE 标志位区域类型映射表 |分配标志位对应的内存区域类型 )
  19. mysql定期清理会话_MySQL会话闲置时间控制
  20. 【自控原理要点】第1章 绪论

热门文章

  1. KingbaseES V8R6运维案例之---wal日志解析DDL操作
  2. Bootstrap 模态框(Modal)的属性及方法
  3. 宽依赖和窄依赖深度剖析
  4. 微信撤回视频服务器还有吗,微信更新又来了,这次终于不会把撤回和删除搞错了...
  5. 部署Microsoft LAPS分步指南
  6. android VideoView的使用例程
  7. 网站不收录的原因分析
  8. ETL 与 ELT的关键区别
  9. 购买条码打印机如何选择才能更适合
  10. 专题五 在Cisco Packet Tracer中设计基于 PT 和 OneNet 的智能家居系统