数据结构与算法(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=0
i=0
while i<len(item) :
if item[i]=='(':
s.push(i)
count+=1
elif item[i]==')':
s.pop()
count-=1
i+=1
#标答
def ParChecker(item):
s=Stack()
balance=True
i=0
while i<len(item) and balance:
a=item[i]
if a=='(':
s.push(a)
else:
if s.isEmpty():
balance=False
else:
s.pop()
#else里的这一步内判断可以说一方面,是pop的必要考量,另一方面,它同时可以作为是否有右括号冗余的情况,因此这里通过balance的True or False进行记录
i+=1
if balance and s.isEmpty():
return True
#由于前面的那个,即使balance=False让那个循环停下了,但那仅仅是跳过循环,这里返回值时必须综合考虑左括号冗余和右括号冗余,也就是上面and连接的两个条件。
else:
return False
-
通用括号匹配
from pythonds.basic.stack import Stack def match(open,closer): opens="([{" closers=")]}" if opens.index(open)==closers.index(closer): return True else: return False #或直接: return opens.index(open)==closers.index(closer) def ParChecker(item): s=Stack() balance=True i=0 while i<len(item) and balance: a=item[i] opens="([{" if a in opens: s.push(a) else: if s.isEmpty(): balance=False else: if match(a,s.peek()): s.pop() else: balance=False i+=1 if balance and s.isEmpty(): return True else: return False
②十进制转化为二进制
③表达式转换(中缀表达式转为后缀表达式)
from pythonds.basic.stack import Stack
def 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 postfix
item=input('请输入中缀表达式:')
change(item)
######④后缀表达式求值
from pythonds.basic.stack import Stack
def 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-oper2
elif op=='*':
return oper1*oper2
elif op=='/':
return oper1/oper2
print(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 Queue
def 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=ppm
self.currenttask=None
self.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 True
else:
return False
#执行新的打印任务
def startNext(self,newtask):
self.currenttask=newtask
self.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.timestamp
def waitTime(self,currentsecond):
return currentsecond-self.timestamp
def getPages(self):
return self.pages
#3 生成作业(实际并不是在生成作业,只是表达了一下概率,用于模拟过程中的if判断)
def newPrintTask():
num=random.randrange(1,141)
if num==140:
return True
else:
return False
#4 模拟运行过程
def simulation(numseconds,ppm):
printer=Printer(ppm) #实例化printer
pq=Queue() #实例化queue
waitingtime=[] #空列表用来存储每个任务的等待时间,优点是即可以存放每一个的时间,又可以计数,便于统计平均值
#通过循环来模拟时间流逝,每一次循环代表一秒,用所在循环的序数作为时间戳
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 Deque
def 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=True
while j<=num/2 and match:
a=deque.removeFront()
b=deque.removeRear()
if a!=b:
match=False
j+=1
return match
#是这样,不过可以不用判断是否是偶数,然后用j+=1,而是只要把条件判断变成deque.size()>0就可啦,即以下
def parChecker(word):
deque=Deque()
for i in word:
deque.addRear(i)
match=True
while deque.size()>0 and match:
a=deque.removeFront()
b=deque.removeRear()
if a!=b:
match=False
j+=1
return 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=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
#2 链表实现
class UnorderedList:
def __init__(self):
self.head=None
def isEmpty(self):
return self.head==None
def add(self,item):
temp=Node(item)
#以下两句的次序非常重要
temp.setNext(self.head)
self.head=temp
def size(self):
current=self.head
count=0
while current!=None:
current=current.getNext()
count+=1
return count
def search(self,item):
current=self.head
found=False
while current!=None and not found:
if current.getData()==item:
found=True
current=current.getNext()
return found
# 乱入式展开一下:这里其实是一个查找算法,然后结合我在算法营学到的一点点知识,其实这个循环可以有多种写法
#1
while current!=None and not found:
if current.getData()==item:
found=True
current=current.getNext()
#2
while current!=None and not found:
if current.getData()==item:
found=True
else:
current=current.getNext()
#这两种写法,总的来说,都可以使它在查找到item的时候停止查找,只不过第二种的current停留在对应的那个上,而第一种的current停留在了下一个即current.getNext()上,也就是比第二种多走了一步。
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())
# 注意:这一句如果写成previous.getNext()=current.getNext() 是不对的!!(这种赋值是赋到current上时用,不修改链表真实结构,但如果真的要对链表进行修改,必须用Node的方法)
# 你看,这个函数的实现就必须用if else(即上面说的第二种类型的写法)才能防止它走过了。
2. 有序表的链表实现
class OrderedList():
#1 和无序表相同的方法
def __init__(self):
self.head=None
def isEmpty(self):
return self.head==None
def size(self):
current=self.head
count=0
while current!=None:
current=current.getNext()
count+=1
return count
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())
#2 和无序表不同的方法
#1.1 搜索:可以在恰当的时候停下来了
def search(self.item):
current=self.head
previous=None
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:
previous=current
current=current.getNext()
return found
#1.2 加入:要先搜索到合适的位置再进行加入(这时就要像remove一样考虑在head处还是在中间了)
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(current) # temp.setNext(self.head)
self.head=temp
else:
temp.setNext(current)
previous.setNext(temp)
三. 递归
(一)递归可视化
递归算法“三定律”:
- 递归算法必须具备基本结束条件
- 递归算法必须逐渐减小规模,改变状态,向基本结束条件演进
- 递归算法必须要调用自身
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 turtle
def 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=0
for ch in line[:-1]:
columnsInMaze=0
rowlist.append(ch)
if ch=='s':
self.startRow=rowsInMaze
self.startCol=columnsInMaze
columnsInMaze+=1
self.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 False
if maze[startRow][startCol]==TRIED or maze[startRow][startCol]==DEAD_END:
return False
if 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=1
else:
n=
if n<0:
return False
return 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=change
if change in coinvaluelist:
return 1
else:
for i in [c for c in coinvaluelist if c<=change]:
numcoins=1+recMc(coinvaluelist,change-i)
if numcoins<mincoins:
mincoins=numcoins
return mincoins
print(recMc([1,5,10,25],63))
#缺点:会有非常多的重复,比如这里我们知道了下一步是15,然后后面连接还有好多步,但我们会遇到很多个下一步是15的情况,然后就造成了大量的重复计算,导致时间耗费超级多
#2 改进版本(用一张表来记录,以消除重复,极大地提高了递归调用次数)
def recMc(coinvaluelist,change,knownResults):
mincoins=change
if change in coinvaluelist:
knownResults[change]=1
return 1
elif 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=numcoins
knownResults[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=cents
for 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]+1
mincoins[cents]=coincount
return 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=cents
newcoin=1
for i in [c for c in coinvaluelist if c<=cents]:
if mincoins[cents-i]+1<coincount:
coincount=mincoins[cents-i]+1
newcoin=i
mincoins[cents]=coincount
coinUsed[cents]=newcoin
return mincoins[change]
def printCoins(coinUsed,change):
coin=change
while 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]=0
return 0
elif (tuple(tr),w) in m:
return m[tuple(tr),w]
else:
vmax=0
for t in tr:
if t[0]<=w:
v=thief(tr-{t},w-t[0])+t[1]
vmax=max(vmax,v)
m[tuple(tr),w]=vmax
return vmax
print(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=0
found=False
while pos<len(alist) and not found:
if alist[pos]==item:
found=True
else:
pos+=1
return found
2.有序表
def sequentialSearch(alist,item):
pos=0
found=False
stop=False
while pos<len(alist) and not found and not stop:
if alist[pos]==item:
found=True
else:
if alist[pos]>item:
stop=True
else:
pos+=1
return found
然而,要注意的是,有序表只是能够在某些情况下相比无序表提前结束循环,但是并不能改变顺序查找算法时间复杂度的数量级,即仍然是O(n)。
(二)二分查找 (Binary Search) 【只适用于有序表】
#1 常规实现
def binarySearch(alist,item):
first=0
last=len(alist)-1
found=False
while first<=last and not found:
midpoint=(first+last)/2
if alist[midpoint]==item:
found=True
else:
if alist[midpoint]>item:
last=midpoint-1
else:
first=midpoint+1
return found
#2 分治策略——递归法
def binarySearch(alist,item):
if len(alist)==0:
return False
else:
midpoint=len(alist)//2
if alist[midpoint]==item:
return True
else:
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)-1 while passnum>0 and exchanges: for i in range(passnum): exchanges=False if alist[i]>alist[i+1]: exchanges=True temp=alist[i] alist[i]=alist[i+1] alist[i+1]=temp passnum=passnum-1 alist=[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=0
for i in range(1,fillslot+1):
if alist[i]>alist[i+1]:
positionOfMax=i
temp=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-1
alist[position]=currentvalue
(五)谢尔排序算法 (Shell Sort)
我们注意到,插入排序的比对次数,在最好的情况下是O(n),这种情况是发生在列表已是有序的情况下,实际上,列表越接近有序,插入排序的比对次数就越少。从这种情况入手,谢尔排序以插入排序作为基础,对无序表进行间隔划分子列表,每个子列表都执行插入排序,同时间隔越来越小,最后一次进行的是标准的插入排序,不过由于这时列表的有序性已经很强了,所以需要进行的操作很少。至于该算法具体的时间复杂度,证明比较复杂故不给出具体证明,不过它的时间复杂度是在O(n)与O(n²)之间的。
def shellSort(alist):
sublistCount=len(alist)//2
while sublistCount>0:
for startposition in range(sublistCount):
gapInsertionSort(alist,startposition,sublistCount)
sublistCount=sublistCount//2
def gapInsertionSort(alist,start,gap):
for i in range(start,len(alist),gap):
currentvalue=alist[i]
position=i
while position>=gap and alist[position-gap]>currentvalue:
alist[position]=alist[position-gap]
position=position-gap
alist[position]=currentvalue
(六)归并排序算法 (Merge Sort)
归并排序是递归算法,是分治策略在排序中的应用,思路是将数据表持续分裂为两半,对两半分别进行归并排序。
#1 常规版
def mergeSort(alist):
if len(alist)>1:
mid=len(alist)//2
lefthalf=alist[:mid]
righthalf=alist[mid:]
mergeSort(lefthalf)
mergeSort(righthalf)
i=j=k=0
while i<len(lefthalf) and j<len(righthalf):
if lefthalf[i]<righthalf[j]:
alist[k]=lefthalf[i]
i+=1
else:
alist[k]=righthalf[j]
j+=1
k+=1
while i<len(lefthalf):
alist[k]=lefthalf[i]
i+=1
k+=1
while j<len(righthalf):
alist[k]=right[j]
j+=1
k+=1
#2 更pythonic的版本
def mergeSort(alist):
if len(alist)<=1:
return alist
mid=len(alist)//2
left=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+1
rightmark=last
done=False
while not done:
while leftmark<=rightmark and alist[leftmark]<=pivotvalue:
leftmark=leftmark+1
while leftmark<=rightmark and alist[rightmark]>=pivotvalue:
rightmark=rightmark-1
#若走出了上述两个循环,则必然是破坏了其中一个条件
if right<leftmark: #如果破坏的是条件1
done=True
else: #如果破坏的是条件2和3
temp=alist[leftmark]
alist[leftmark]=alist[rightmark]
alist[rightmark]=temp
#排序完毕后,将“中值”点移到它该在的位置
temp0=alist[first]
alist[first]=alist[rightmark]
alist[rightmark]=temp0
return 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=0
for 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]=key
self.data[hashvalue]=data
#若该处不为空
else:
#如果对应的就是这个key值,那么就replace
if 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]=key
self.data[nextslot]=data
#退出循环的原因若是 “找到了一个对应key的位置”
else:
self.data[nextslot]=data #replace
# get方法
def get(self,key):
startslot=self.hashfuction(key) #通过哈希函数得到key对应的编码
data=None
stop=False
found=False
position=startslot
while self.slots[position]!=None and not found and not stop:
#如果非常顺利地直接得到,那么就找到啦
if self.slots[position]==key:
found=True
data=self.data[position]
#如果没有,那么就考虑,进行线性探测,若绕了一圈回到原位还没找到就说明没有
else:
position=self.rehash(position) #进行线性探测,然后回到while循环开头进行if判断
#如果绕了一圈回到原位,则说明没有,可停止,返回最初的None
if position==startslot:
stop=True
return 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 root
def insertRight(root,newBranch):
t=root.pop(2)
if len(t)>1:
root.insert(2,[newBranch,[],t])
else:
root.insert(2,[newBranch,[],[]])
return root
def getRootVal(root):
return root[0]
def setRootVal(root):
root[0]=newval
def getLeftChild(root):
return root[1]
def getRightChild(root):
return root[2]
2.链表实现
class BinaryTree:
def __init__(self,rootObj):
self.key=rootObj
self.leftChild=None
self.rightChild=None
def insertLeft(self,newNode):
if self.leftChild==None:
self.leftChild=newNode
else:
t=BinaryTree(newNode)
t.leftChild=self.leftChild
self.leftChild=t
def insertRight(self,newNode):
if self.rightChild==None:
self.rightChild=newNode
else:
t=BinaryTree(newNode)
t.rightChild=self.rightChild
self.rightChild=t
def getRootVal(self):
return self.key
def setRootVal(self,obj):
self.key=obj
def getLeftChild(self):
return self.leftChild
def getRightChild(self):
return self.rightChild
(二)树的应用 —— 表达式解析
1. 建立表达式解析树
def buildParseTree(fpexp):
fplist=fpexp.split()
pstack=Stack()
eTree=BinaryTree('')
pstack.push(eTree)
currentTree=eTree
for 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 ValueError
return 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=None
res2=None
if 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)。
- ADT BinaryHeap的操作定义如下:
bh=BinaryHeap() #创建一个空二叉堆对象
bh.insert(k) #将新key加入到堆中
bh.findMin() #返回堆的最小项,最小项仍保留
bh.delMin() #返回堆的最小项,同时从堆中删除
bh.isEmpty() #返回堆是否为空
bh.size() #返回堆中key的个数
bh.bulidHeap(list) #从一个key列表创建新堆
- 二叉堆操作的实现
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]=temp
i=i//2
def insert(self,k):
self.heapList.append(k)
self.currentSize+=1
self.percUp(self.currentSize) #调用【上升】操作的函数
#3 移除最小者的操作
def minChild(self,i): #确认并获取两个子节点中更小的那个节点
if i*2+1>self.currentSize: #唯一子节点
return i*2
else: #两侧都有的正常情况
if self.heapList[i*2]<self.heapList[i*2+1]:
return i*2
else:
return i*2+1
def 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]=tmp
i=mc
def delMin(self):
retval=self.heapList[1]
self.heapList[1]=self.heapList[self.currentSize]
self.currentSize=self.currentSize-1
self.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-1
print(self.heapList,i)
(五)二叉查找树(BST)及其基本操作
二叉查找树BST的性质:比父节点小的key都出现在左子树,比父节点大的key都出现在右子树(对于每一个节点都是如此)。
二叉查找树的实现:节点和链接结构,需要用到BST和TreeNode两个类,BST的root成员引用根节点TreeNode
# BST
class BinarySearchTree:
def __init__(self):
self.root=None
self.size=0
def length(self):
return self.size
def __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=key
self.payload=val
self.leftChild=left
self.rightChild=right
self.parent=parent
def hasLeftChild(self):
return self.leftChild
def hasRightChild(self):
return self.rightChild
def isLeftChild(self):
return self.parent and self.parent.leftChild==self
def isRightChild(self):
return self.parent and self.parent.rightChild==self
def isRoot(self):
return not self.parent
def isLeaf(self):
return not (self.rightChild or self.leftChild)
def hasAnyChild(self):
return self.leftChild or self.rightChild
def hasBothChild(self):
return self.leftChild and self.rightChild
def replaceNodeData(self,key,value,lc,rc):
self.key=key
self.payload=value
self.leftChild=lc
self.rightChild=rc
if self.hasLeftChild():
self.leftChild.parent=self
if 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+=1
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,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.payload
else:
return None
else:
return None
def _get(self,key,currentNode):
if not currentNode:
return None
elif currentNode.key==key:
return currentNode
elif 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 True
else:
return False
def __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-1
else:
raise KeyError('Error,key not in tree')
elif self.size==1 and self.root.key=key:
self.root=None
self.size=self.size-1
else:
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=None
else:
currentNode.parent.rightChild=None
#第二种情形:该节点有且仅有一个子节点
else:
if currentNode.hasLeftChild():
if currentNode.isLeftChild():
currentNode.leftChild.parent=currentNode.parent
currentNode.parent.leftChild=currentNode.leftChild
elif currentNode.isRightChild():
currentNode.rightChild.parent=currentNode.parent
currentNode.parent.leftChild=currentNode.leftChild
else:
currentNode.replaceNodeData(currentNode.leftChild.key,
currentNode.leftChild.payload,
currentNode.leftChild.leftChild,
currentNode.leftChild.rightChild)
else:
if currentNode.isRightChild():
currentNode.rightChild.parent=currentNode.parent
currentNode.parent.rightChild=currentNode.rightChild
elif currentNode.isLeftChild():
currentNode.rightChild.parent=currentNode.parent
currentNode.parent.leftChild=currentNode.rightChild
else:
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.key
currentNode.payload=succ.payload
#声明2 以下是对于TreeNode类设定的方法
#1 寻找后继节点
def findSuccessor(self):
succ=None
if self.hasRightChild():
succ=self.rightChild.findMin()
#[]这一块在这个算法里用不到
else:
if self.parent:
if self.isLeftChild():
succ=self.parent #左侧连环移相当于直接删掉左侧正数第二个相当于用parent代替它
else:
self.parent.rightChild=None
succ=self.parent.findSuccessor()#这个…就相当于去左边找?之类的…
self.parent.rightChild=self
#[\]
return succ
def findMin(self): #即“到左下角”
current=self
while current.hasLeftChild():
current=current.leftChild
return current
#2 摘出节点
def spliceOut(self):
if self.isLeaf():
if self.isLeftChild():
self.parent.leftChild=None
else:
self.parent.rightChild=None
elif self.hasAnyChildren():
if self.hasLeftChild(): #在这个题中是不可能的,但是为了逻辑的完整性,还是写上
if self.isLeftChild():
self.parent.leftChild=self.leftChild
else:
self.parent.rightChild=self.rightChild
else:
if self.isLeftChild():
self.parent.leftChild=self.leftChild
else:
self.parent.rightChild=self.rightChild
self.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) #【新函数调用】
return
if node.parent!=None:
#作为左右节点对于factor的影响,相当于是一个factor的计算过程,从它末端开始,倘若它不为0,就往上传递影响
#它对于父节点的影响
if node.isLeftChild():
node.parent.balanceFactor+=1
elif 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.leftChild
if newRoot.leftChild!=None:
newRoot.leftChild.parent=rotRoot
#part2 修改newRoot的根节点
#向上的指针
newRoot.parent=rotRoot.parent
#向下的指针
if rotRoot.isRoot():
self.root=newRoot
else:
if rotRoot.isLeftChild():
rotRoot.parent.leftChild=newRoot
else:
rotRoot.parent.rightChild=newRoot
#part3 修改newRoot的子节点
newRoot.leftChild=rotRoot #向下指针
rotRoot.parent=newRoot #向上指针
#至于那个可能的“替代理论”已经在part1讨论过了(不过那只是一种对于“如果”的解决方案)
#part4 修改两个节点的factor
rotRoot.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=key
self.connectedTo={}
def addNeighbor(self,nbr,weight=0): #增加邻接节点
self.connecterTo[nbr]=weight
def __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): #获得它的id
return self.id
def getWeight(self,nbr): #获得某个连接的权重
return self.connectedTo[nbr]
#2 图Graph
class Graph:
def __init__(self): #初始化方法,创建一个空字典用来存储所有的节点,创建一个变量用来计数
self.vertList={}
self.numVertices=0
def addVertex(self,key): #加入新的节点(实例化一个节点类,然后把它与相应的键以键值对的形式存储到节点字典中)
self.numVertices+=1
newVertex=Vertex(key)
self.vertList[key]=newVertex
return newVertex
def getVertex(self,k): #得到某个节点
if k in self.vertList:
return self.vertList[k]
def __contains__(self,n): #使用x in xxx 这种返回True or False
return n in self.vertList
def 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={} #创建一个字典,用来存储不同的bucket
g=Graph() #创建一个图
wfile=open(wordfile,'r') #读取包含相应单词的文档
#创建bucket并将word写入bucket
for line in wfile:
word=line[:-1] #取到word
#构建bucket并写入
for i in range(len(word)): #获得bucket
bucket=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) #将初始节点距离设为0
start.setPred(None) #将初始节点的父节点设置为None
verQueue=Queue() #创建一个空队列,用来装等待操作的节点(此处,操作是指“搜索其所有的邻接节点,并判断颜色,黑色略过,白色则变灰、设置距离并加入队列末端(其实也就相当于开启下一行啦,但是不重要啊,顺序读取不就是这样吗)”)
vertQueue.enqueue(start) #把初始节点加入队列
while (vertQueue.size()>0):
currentVert=vertQueue.dequeue() #顺序从队列中读取灰色节点,完成其nbr的全部处理后设置为黑色
for nbr in currentVert.getConnections(): #处理该灰色节点的所有白色nbr
if (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=y
while (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) #对于每一个格子都标注好其特定的nodeId
newPositions=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)] # “🐎”所有的可能的步伐方向
for i in moveOffsets:
#进行每一个步伐的行走
newX=x+i[0]
newY=y+i[1]
if legalCoord(newX,bdSize) and legalCoord(newY,bdSize): #如果走下去的位置不出界
newMoves.append((newX,newY)) #则把这一步走出来的合法下一步加入到列表里
return newMoves
#4 判断所得到的位置是否出界的小函数
def legalCoord(x,bdSize):
if x>=0 and x<bdSize:
return True
else:
return False
2.周游算法
#这是一个递归算法
def knightTour(n,path,u,limit): #n是当前所在的深度,u是当前节点,limit是总个数(可以说是终止条件也可以说是目的)
u.setColor('gray') #调灰
path.append(u) #入栈
#对于当前节点进行操作了
if n<limit: #0.1 倘若暂时还没有达成
nbrList=list(u.getConnections()) #全部的nbr
i=0
done=False
while i<len(nbrList) and not done: #倘若还没有完成,就循环把所有的nbr都使用( 不过要注意,因为它是递归调用,所以每一个都是一路先走到最深处,出来再考虑上层的)
if nbrList[i].getColor=='white': #要考虑的一定是白色节点
done=knightTour(n+1,path,nbrList[i],limit) #递归调用自身,探索下一层
i+=1
if not done: #如果把当前节点的全部子节点都尝试过了,没有任何通路,则将当前节点pop出来并设置成白色(即进行回溯操作)
path.pop() #出栈
u.setColor('white') #设置成白色
else: #0.2 倘若已达成
done=True
return done
3.周游算法改进 —— Warnsdorff算法
前面那个算法是指数复杂度,性能极差,不过幸运的是,即使是指数时间复杂度算法也能在实际性能上加以大幅度改进,这个改进算法被特别以发明者名字命名:Warnsdorff算法。初始算法中nbeList直接以原始顺序来确定深度优先搜索的分支次序;新的算法仅修改了遍历下一格的次序——将u的合法移动目标棋盘格排序为:具有最少合法移动目标的格子优先搜索。
def orderByAvail(n):
resList=[]
for v in n.getConnections(): #获得所有nbr
if v.getColor=='white':
#计数其每个nbr的nbr数量
c=0
for w in v.getConnections():
if w.getColor=='white':
c=c+1
resList.append(c,v) #将它的nbr连带着nbr的nbr数量一起加入resList
resList.sort(key=lambda x:x[0]) #按照各个nbr的nbr数量给nbr排序
return [y[1] for y in resList] #返回resList
像这样采用先验知识来改进算法性能的做法,称作“启发式规则heuristic”。启发式规则经常应用于人工智能领域,可以有效减少搜索范围、更快达到目标。如棋类算法,会预先存入棋谱、布阵口诀、高手相关等“启发式规则”,能够在最短时间内从海量的棋局落子点搜索树中定位最佳落子。
(四)通用的深度优先搜索(DFS)
from pythonds.graphs import Graph
class DFSGraph(Graph):
def __init__(self):
super().__init__()
self.time=0
def dfs(self):
for aVertex in self:
aVertex.setColor('white')
aVertex.setPred(-1)
for aVertex in self: #如果还有未包括的项点,则建森林
if aVertex.getColor()=='white':
self.dfsvisit(aVertex)
def dfsvisit(self,startVertex):
startVertex.setColor('gray')
self.time+=1
startVertex.setDiscovery(self.time) #记录发现时间
for nextVertex in startVertex.getConnections():
if nextVertex.getColor=='white':
nextVertex.setPred(startVertex)
self.dfsvisit(nextVertex) #递归调用自身,去处理它的邻接节点
startVertex.setColor('black')
self.time+=1
startVertex.setFinish(self.time) #记录截止时间
BFS采用队列存储待访问顶点,DFS则是通过递归调用,隐式使用了栈。
DFS的算法中有两个for循环,每个都是|V|次,所以时间复杂度为O(|V|),而对其邻接节点实际上相当于搜索了它的所有边,所以是O(|E|),加起来就是和BFS一样的O(|V|+|E|)。
(五)图的应用:带权图的最短路径——Dijkstra算法
Dijkstra算法是一个迭代算法,得出带权图中从一个顶点到其余所有顶点的最短路径,很接近于BFS算法的结果。
from pythonds.graphs import PriorityQueue,Graph,Vertex
def dijkstra(aGraph,start):
pq=PriorityQueue() #创建优先队列
start.setDistance(0) #将顶点距离设置为0
pq.buildHeap([(v.getDistance(),v) for v in aGraph]) #建堆
while not pq.isEmpty():
currentVert=pq.delMin() #按顺序选取节点出堆
for nextVert in currentVert.getConnections(): #对于当前顶点的所有子节点进行操作(下面的操作是修改它们的Dist)
newDist=currentVert.getDistance()+currentVert.getWeight(nextVert) #计算出新路径中的距离值
if newDist<nextVert.getDistance(): #如果比原本距离要小,则替代
nextVert.setDistance(newDist)
nextVert.setPred(currentVert)
pq.decreaseKey(nextVert,newDist)
该算法时间复杂度为O((|V|+|E|)log|V|)
需要注意的是,Dijkstra算法只能处理大于0的权重,如果图中出现负数权重,则算法会陷入无限循环。
虽然Dijkstra算法完美解决了带权图的最短路径问题,但实际上Internet的路由器中采用的是其他算法。其中最重要的原因是,该算法需要具备整个图的数据,但对于Internet的路由器来说,显然无法将整个Internet的所有路由器及其连接信息保存到本地,这不仅是数据量的问题,Internet动态变化的特性也使得保存全图缺乏现实性。路由器的选径算法对于互联网极其重要,可以参考“距离向量路由算法”。
(六)图的应用:最小生成树
生成树:拥有图中所有的顶点和最少数量的边,以保持联通的子图。
from pythonds.graphs import PriorityQueue,Graph,Vertex
import sys
def prim(G,start):
pq=PriorityQueue() #创建一个优先队列
for v in G: #先给所有的节点设置好distance
v.setDistance(sys.maxsize)
v.setPred(None)
start.setDistance(0) #再修改第一个节点的distance为0
pq.buildHeap([(v.getDistance(),v) for v in G]) #建堆
while not pq.isEmpty():
currentVert=pq.delMin() #按照顺序从堆中取节点
for nextVert in currentVert.getConnections():
#对于与节点相连接的所有边,进行操作(这里的操作是贪心算法不断更新最小距离的边,并让其挤到前面去)
newCost=currentVert.getWeight(nextVert)
if nextVert in pq and newCost<nextVert.getDistance(): #一方面进行是否合法的判断,另一方面看它是不是更小的
nextVert.setPred(currentVert)
nextVert.setDistance(newCost)
pq.decreaseKey(nextVert,newCost)