from timeit import Timer
def test1():
l=[]
for i in range(1000):#for循环生成列表
l=l+[i]
def test2():
l = []
for i in range(1000):#追加方法生成列表
l.append(i)
def test3():
l=[i for i in range(1000)]#列表解析式
def test4():
l=list(range(1000))#列表构造器
t1 = Timer( "test1() ","from 大O表示法 import test1" )
print ( " concat ", t1.timeit (number=1000 ),"milliseconds " )
t2 = Timer ( "test2() ", "from 大O表示法 import test2" )#大O表示法是代码所在文件名
print ( " append ", t2.timeit (number=1000 ), "milliseconds" )
t3 = Timer ( "test3 ( )", "from 大O表示法 import test3" )
print ( " comprehension ", t3.timeit (number=1000), "milliseconds " )
t4 = Timer ( "test4 ()", "from 大O表示法 import test4")
print ("list range " , t4.timeit (number=1000 ),"milliseconds " )
import timeit
popzero=timeit.Timer("x.pop(0)",
"from pop性能分析 import x")
popend=timeit.Timer("x.pop()",
"from pop性能分析 import x")
x=list(range(2000000))
print(popzero.timeit(number=1000))
x=list(range(2000000))
print(popend.timeit(number=1000))
字典的取值操作和赋值操作都是常数阶,包含(检查某个键是否在字典中),它也是常数阶。
import timeit
import random
for i in range(10000,1000001,20000):
t=timeit.Timer("random.randrange(%d) in x"%i,"from 比较列表和字典的包含操作 import random,x")
x=list(range(i))
lst_time=t.timeit(number=1000)
x={j:None for j in range(i)}
d_time=t.timeit(number=1000)
print("%d,%10.3f,%10.3f"%(i,lst_time,d_time))
LIFO ( last-in first-out ),即后进先出
栈可用于反转元素的排列顺序
考虑到栈的反转特性,我们可以想到在使用计算机时的一些例子。例如,每一个浏览器都有返回按钮。当我们从一个网页跳转到另一个网页时,这些网页——实际上是URL——都被存放在一个栈中。当前正在浏览的网页位于栈的顶端,最早浏览的网页则位于底端。如果点击返回按钮,便开始反向浏览这些网页。
假设s是一个新创建的空栈。表3-1展示了对s进行一系列操作的结果。在“栈内容”一列中,栈顶端的元素位于最右侧。
抽象数据类型的实现常被称为数据结构。
因为栈是元素的集合,所以完全可以利用Python提供的强大、简单的原生集合来实现。这里,我们将使用列表。
Python列表是有序集合,它提供了一整套方法。举例来说,对于列表[2,5,3,6,7,4],只需要考虑将它的哪一边视为栈的顶端。一旦确定了顶端,所有的操作就可以利用append和pop 等列表方法来实现。
代码清单3-1是栈的实现,它假设列表的尾部是栈的顶端。当栈增长时(即进行push操作),新的元素会被添加到列表的尾部。pop操作同样会修改这一端。
class Stack:
def __init__(self):
self.items=[]
def isEmpty(self):
return self.items==[]
def push(self,item):
self.items.append(item)
def pop(self):
return self.items.pop()
def peek(self):
return self.items[len(self.items)-1]
def size(self):
return len(self.items)
s=Stack()
# print(s.isEmpty())
s.push(4)
s.push('dog')
# print(s.peek())
s.push(True)
# print(s.size())
# print(s.isEmpty())
s.push(8.4)
print(s.pop())
print(s.pop())
print(s.size())
值得注意的是,也可以选择将列表的头部作为栈的顶端。不过在这种情况下,便无法直接使用pop方法和 append方法,而必须要用pop方法和insert方法显式地访问下标为0的元素,即列表中的第1个元素。代码清单3-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.items[0]
def size(self):
return len(self.items)
append方法和 pop()方法的时间复杂度都是O(1),这意味着不论栈中有多少个元素,第一种实现中的push操作和 pop操作都会在恒定的时间内完成。
第二种实现的性能则受制于栈中的元素个数,这是因为insert (0)和pop(0)的时间复杂度都是O(n),元素越多就越慢。显而易见,尽管两种实现在逻辑上是相等的,但是它们在进行基准测试时耗费的时间会有很大的差异。
from stackModule import Stack
def parChecker(symbolString):
s=Stack()
balanced=True
index=0
while index<len(symbolString) and balanced:
symbol=symbolString[index]
if symbol=="(":
s.push(symbolString)
else:
if s.isEmpty():
balanced=False
else:
s.pop()
index=index+1
if balanced and s.isEmpty():
return True
else:
return False
from stackModule import Stack
def parChecker(symbolString):
s=Stack()
balanced=True
index=0
while index<len(symbolString) and balanced:
symbol=symbolString[index]
if symbol in "([{":
s.push(symbol)
else:
if s.isEmpty():
balanced=False
else:
top=s.pop()
if not matches(top,symbol):
balanced=False
index=index+1
if balanced and s.isEmpty():
return True
else:
return False
def matches(open,close):
opens="([{"
closers=")]}"
return opens.index(open)==closers.index(close)
print(parChecker("{}("))
如何才能简便地将整数值转换成二进制数呢?答案是利用一种叫作“除以2”的算法,该算法使用栈来保存二进制结果的每一位。
“除以2”算法假设待处理的整数大于0。它用一个简单的循环不停地将十进制数除以2,并且记录余数。第一次除以2的结果能够用于区分偶数和奇数。如果是偶数,则余数为0,因此个位上的数字为0;如果是奇数,则余数为1,因此个位上的数字为1。可以将要构建的二进制数看成一系列数字;计算出的第一个余数是最后一位。如图3-5所示,这又一次体现了反转特性,因此用栈来解决该问题是合理的。
from stackModule import Stack
def parChecker(symbolString):
s=Stack()
balanced=True
index=0
while index<len(symbolString) and balanced:
symbol=symbolString[index]
if symbol in "([{":
s.push(symbol)
else:
if s.isEmpty():
balanced=False
else:
top=s.pop()
if not matches(top,symbol):
balanced=False
index=index+1
if balanced and s.isEmpty():
return True
else:
return False
def matches(open,close):
opens="([{"
closers=")]}"
return opens.index(open)==closers.index(close)
print(parChecker("{}("))
from stackModule import Stack
def baseConverter(decNumber,base):
digits="01234567890ABCDEF"
remstack=Stack()
while decNumber>0:
rem=decNumber%base
remstack.push(rem)
decNumber=decNumber//base
newString=""
while not remstack.isEmpty():
newString=newString+digits[remstack.pop()]
return newString
print(baseConverter(23333,16))
from stackModule import Stack
import string
# 从左往右扫描这个标记列表。
# 如果标记是操作数,将其添加到结果列表的末尾。如果标记是左括号,将其压入 opstack栈中。
# 如果标记是右括号,反复从opstack栈中移除元素,直到移除对应的左括号。将从栈中取出的每一个运算符都添加到结果列表的末尾。
# 如果标记是运算符,将其压入opstack栈中。但是,在这之前,需要先从栈中取出优先级更高或相同的运算符,并将它们添加到结果列表的末尾。
# (4)当处理完输人表达式以后,检查opstack。将其中所有残留的运算符全部添加到结果列表的末尾。
def infixToPostfix(infixexpr):
prec={}
prec["*"]=3
prec["/"] = 3
prec["+"] = 2
prec["-"] = 2
prec["("] = 1
opStack=Stack()#放运算符
postfixList=[]#存放生成的后续列表
tokenList=infixexpr.split()#把传入的字符串分割开,形成含有多个字符的列表
print(tokenList)
for token in tokenList:
if token in string.ascii_uppercase:
postfixList.append(token)
elif token=='(':
opStack.push(token)#压入符号站
elif token==')':
topToken=opStack.pop()
while topToken != '(':
postfixList.append(topToken)
topToken=opStack.pop()
else:
while (not opStack.isEmpty()) and (prec[opStack.peek()] >= prec[token]):
postfixList.append(opStack.pop)
opStack.push(token)
while not opStack.isEmpty():
postfixList.append(opStack.pop())
return " ".join(postfixList)
print(infixToPostfix("( A + B ) * ( C + D )"))
print(infixToPostfix("( A + B ) * C"))
print(infixToPostfix("A + B * C"))
from stackModule import Stack
def postfixEval(postfixExpr):
operandStack=Stack()
tokenList=postfixExpr.split()#把字符串转换成一个个字符组成的列表
for token in tokenList:
if token in "0123456789":
operandStack.push(int(token))#把数字压入栈中
else:
operand2=operandStack.pop()
operand1=operandStack.pop()
result=doMath(token,operand1,operand2)
operandStack.push(result)
return operandStack.pop()
def doMath(op,op1,op2):
if op=="*":
return op1*op2
elif op=="/":
return op1/op2
elif op == "+":
return op1 +op2
else:
return op1 -op2
print(postfixEval("7 8 + 3 2 + /"))
class Queue:
def __init__(self):
self.items=[]
def isEmpty(self):
return self.items==[]
def enqueue(self,item):
self.items.insert(0,item)
def dequeue(self):
return self.items.pop()
def size(self):
return len(self.items)
q=Queue()
print(q.isEmpty())
q.enqueue('dog')
q.enqueue(4)
print(q.size())
q.enqueue(True)
q.enqueue(8.4)
print(q.dequeue())
print(q.dequeue())
from queueModule import Queue
def hotPotato(nameList,num):
simqueue=Queue()
for name in nameList:
simqueue.enqueue(name)
while simqueue.size()>1:
for i in range(num):
simqueue.enqueue(simqueue.dequeue())
simqueue.dequeue()
return simqueue.dequeue()
print(hotPotato(["bill","david","susan","jane","kent","brat"],7))
class Printer:
def __init__(self,ppm):
self.pagerate=ppm
self.currentTask=None
self.timeRemaining=0
def tick(self):
if self.currentTask!=None:
self.timeRemaining=self.timeRemaining-1
if self.timeRemaining<=0:
self.currentTask=None
def busy(self):
if self.currentTask!=None
return True
else:
return False
def startNext(self,newtask):
self.currentTask=newtask
self.timeRemaining=newtask.getPages()*60/self.pagerate
import random
class Task:
def __init__(self,time):
self.timestamp=time
self.pages=random.randrange(1,21)
def getPages(self):
return self.pages
def waitTime(self,currenttime):
return currenttime-self.timestamp
每一个任务都需要保存一个时间戳,用于计算等待时间。这个时间戳代表任务被创建并放人打印任务队列的时间。waitTime方法可以获得任务在队列中等待的时间。
def simulation(numSeconds,pagesPerMinute):
labprinter=Printer(pagesPerMinute)
printQueue=Queue()
waitingtimes=[]
for currentSecond in range(numSeconds):
if newPrintTask():
task=Task(currentSecond)
printQueue.enqueue(task)
if (not labprinter.busy()) and (not printQueue.isEmpty()):
nexttask=printQueue.dequeue()
waitingtimes.append(nexttask.waitTime(currentSecond))
labprinter.startNext(nexttask)
labprinter.tick()
averageWait=sum(waitingtimes)/len(waitingtimes)
print("average wait %6.2f secs %3d tasks remaining."%(averageWait,printQueue.size()))
def newPrintTask():
num=random.randrange(1,181)
if num==180:
return True
else:
return False
import random
from queueModule import Queue
# 我们创建3个类:Printer、Task和 PrintQueue。它们分别模拟打印机、打印任务和队列。
# Printer类(代码清单3-11)需要检查当前是否有待完成的任务。
# 如果有,那么打印机就处于工作状态(第13~17行),并且其工作所需的时间可以通过要打印的页数来计算。
# 其构造方法会初始化打印速度,即每分钟打印多少页。tick 方法会减量计时,
# 并且在执行完任务之后将打印机设置成空闲状态(第11行)。
class Printer:
def __init__(self,ppm):
self.pagerate=ppm#打印机速度,每分钟打印的页数
self.currentTask=None#任务进打印机后剩余的执行时间
self.timeRemaining=0
def tick(self):
if self.currentTask!=None:
self.timeRemaining=self.timeRemaining-1
if self.timeRemaining<=0:
self.currentTask=None
def busy(self):
if self.currentTask!=None:
return True
else:
return False
def startNext(self,newtask):
self.currentTask=newtask
self.timeRemaining=newtask.getPages()*60/self.pagerate
# Task类(代码清单3-12)代表单个打印任务。当任务被创建时,随机数生成器会随机提供页数,取值范围是1~20。
# 我们使用random模块中的randrange 函数来生成随机数。
class Task:
def __init__(self,time):
self.timestamp=time#任务进入队列时间
self.pages=random.randrange(1,21)
def getPages(self):
return self.pages
def waitTime(self,currenttime):
return currenttime-self.timestamp
# 每一个任务都需要保存一个时间戳,用于计算等待时间。这个时间戳代表任务被创建并放人打印任务队列的时间。
# waitTime方法可以获得任务在队列中等待的时间。
def simulation(numSeconds,pagesPerMinute):
labprinter=Printer(pagesPerMinute)
printQueue=Queue()
waitingtimes=[]#存放每个任务完成所需要的时间
# 对numSeconds里面的每一秒进行检查
for currentSecond in range(numSeconds):
# 在currentSecond秒时产生了新任务
if newPrintTask():
task=Task(currentSecond)#创建任务
printQueue.enqueue(task)#把任务加入队列
if (not labprinter.busy()) and (not printQueue.isEmpty()):
nexttask=printQueue.dequeue()
waitingtimes.append(nexttask.waitTime(currentSecond))#从任务创建到进入打印机时间
labprinter.startNext(nexttask)
labprinter.tick()
averageWait=sum(waitingtimes)/len(waitingtimes)
print("average wait %6.2f secs %3d tasks remaining."%(averageWait,printQueue.size()))
def newPrintTask():
num=random.randrange(1,181)
if num==180:
return True
else:
return False
for i in range(10):
simulation(3600,10)
和前几节一样,我们通过创建一个新类来实现双端队列抽象数据类型。Python列表再一次提供了很多简便的方法来帮助我们构建双端队列。在代码清单3-14中,我们假设双端队列的后端是列表的位置0处。
class Deque:
def __init__(self):
self.items=[]
def isEmpty(self):
return self.items==[]
def addFront(self,item):
self.items.append(item)
def addRear(self,item):
self.items.insert(0,item)
def removeFront(self):
return self.items.pop()
def removeRear(self):
return self.items.pop(0)
def size(self):
return len(self.items)
d=Deque()
print(d.isEmpty())
d.addRear(4)
d.addRear('dog')
d.addFront('CAT')
d.addFront(True)
print(d.size())
print(d.isEmpty())
d.addRear(8.4)
print(d.removeRear())
print(d.removeFront())
from dequeModule import Deque
def palChecker(aString):
chardeque=Deque()#新建一个双端队列
for ch in aString:
chardeque.addRear(ch)
stillEqual=True
while chardeque.size()>1 and stillEqual:
first=chardeque.removeFront()
last=chardeque.removeRear()
if first!=last:
stillEqual=False
return stillEqual
print(palChecker('lsdkjfskf'))
print(palChecker('toot'))
节点( node)是构建链表的基本数据结构。每一个节点对象都必须持有至少两份信息。首先,节点必须包含列表元素,我们称之为节点的数据变量。其次,节点必须保存指向下一个节点的引用。代码清单3-16展示了Node类的 Python实现。在构建节点时,需要为其提供初始值。执行下面的赋值语句会生成一个包含数据值93的节点对象,如图3-20所示。需要注意的是,一般会像图3-21所示的那样表示节点。Node类也包含访问和修改数据的方法,以及指向下一个元素的引用。
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
如前所述,无序列表(unordered list)是基于节点集合来构建的,每一个节点都通过显式的引用指向下一个节点。只要知道第一个节点的位置(第一个节点包含第一个元素),其后的每一个元素都能通过下一个引用找到。因此,UnorderedList类必须包含指向第一个节点的引用。代码清单3-17展示了UnorderedList类的构造方法。注意,每一个列表对象都保存了指向列表头部的引用。
class UnorderList:
def __init__(self):
self.head=None
在代码清单3-18中, isEmpty方法检查列表的头部是否为指向None的引用。布尔表达式self.head == None当且仅当链表中没有节点时才为真。由于新的链表是空的,因此构造方法必须和检查是否为空的方法保持一致。这体现了使用None表示链表末尾的好处。在Python中,None可以和任何引用进行比较。如果两个引用都指向同一个对象,那么它们就是相等的。我们将在后面的方法中经常使用这一特性。
def isEmpty(self):
return self.head==None
为了将元素添加到列表中,需要实现 add方法。但在实现之前,需要解决一个重要问题:新元素要被放在链表的哪个位置?由于本例中的列表是无序的,因此新元素相对于已有元素的位置并不重要。新的元素可以在任意位置。因此,将新元素放在最简便的位置是最合理的选择。
由于链表只提供一个人口(头部),因此其他所有节点都只能通过第一个节点以及 next 链接来访问。这意味着添加新节点最简便的位置就是头部,或者说链表的起点。我们把新元素作为列表的第一个元素,并且把已有的元素链接到该元素的后面。
代码清单3-19展示了add方法的实现。列表中的每一个元素都必须被存放在一个节点对象中。第2行创建一个新节点,并且将元素作为其数据。现在需要将新节点与已有的链表结构链接起来。这一过程需要两步,如图3-24所示。第1步(第3行),将新节点的next引用指向当前列表中的第一个节点。这样一来,原来的列表就和新节点正确地链接在了一起。第2步,修改列表的头节点,使其指向新创建的节点。第4行的赋值语句完成了这一操作。
def add(self,item):
temp=Node(item)
temp.setNext(self.head)
self.head=temp
def length(self):
current=self.head
count=0
while current!=None:
count=count+1
current=current.getNext()
return count
在无序列表中搜索一个值同样也会用到遍历技术。每当访问一个节点时,检查该节点中的元素是否与要搜索的元素相同。在搜索时,可能并不需要完整遍历列表就能找到该元素。事实上,如果遍历到列表的末尾,就意味着要找的元素不在列表中。如果在遍历过程中找到所需的元素,就没有必要继续遍历了。
代码清单3-21展示了search方法的实现。与在 length方法中相似,遍历从列表的头部开始(第2行)。我们使用布尔型变量found来标记是否找到所需的元素。由于一开始时并未找到该元素,因此第3行将found 初始化为False。第4行的循环既考虑了是否到达列表末尾,也考虑了是否已经找到目标元素。只要还有未访问的节点并且还没有找到目标元素,就继续检查下一个节点。第5行检查当前节点中的元素是否为目标元素。如果是,就将found设为Trueo
def search(self,item):
current=self.head
found=False
while current!=None and not found:
if current.getData()==item:
found=True
else:
current=current.getNext()
return found
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())
from nodeModule import Node
class UnorderList:
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 length(self):
current=self.head
count=0
while current!=None:
count=count+1
current=current.getNext()
return count
def search(self,item):
current=self.head
found=False
while current!=None and not found:
if current.getData()==item:
found=True
else:
current=current.getNext()
return found
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())
myList=UnorderList()
myList.add(31)
myList.add(77)
myList.add(17)
myList.add(93)
myList.add(26)
myList.add(54)
print(myList.search(17))
在实现有序列表时必须记住,元素的相对位置取决于它们的基本特征。整数有序列表17,26,31,54,77,93可以用如图3-32所示的链式结构来表示。
class orderedList:
def __init__(self):
self.head=None
因为isEmpty和 length仅与列表中的节点数目有关,而与实际的元素值无关,所以这两个方法在有序列表中的实现与在无序列表中一样。同理,由于仍然需要找到目标元素并且通过更改链接来移除节点,因此remove方法的实现也一样。剩下的两个方法,search和 add,需要做一些修改。
代码清单3-24展示了完整的search方法。通过增加新的布尔型变量stop,并将其初始化为False(第4行),可以将上述条件轻松整合到代码中。当stop是False时,我们可以继续搜索链表(第5行)。如果遇到其值大于目标元素的节点,则将stop设为True(第9~10行)。之后的代码与无序列表中的一样。
def search(self,item):
current=self.head
found=False
stop=False
while current!=None and not found and not stop:
if current.getData()==item:
found=True
else:
if current.getData()>item:
stop=True
else:
current=current.getNext()
return found
和无序列表一样,由于 current无法提供对待修改节点的访问,因此使用额外的引用previous是十分必要的。代码清单3-25展示了完整的add方法。第23行初始化两个外部引用,第910行保证previous一直跟在current后面。只要还有节点可以访问,并且当前节点的值不大于要插入的元素,判断条件就会允许循环继续执行。在循环停止时,就找到了新节点的插入位置。
def add(self,item):
current=self.head
previous=None
stop=False
while current!=None and not stop:
if current.getData()>item:
stop=True
else:
previous=current
current=current.getNext()
temp=Node(item)
if previous==None:
temp.setNext(self.head)
self.head=temp
else:
temp.setNext(current)
previous.setNext(temp)
递归是解决问题的一种方法,它将问题不断地分成更小的子问题,直到子问题可以用普通的方法解决。通常情况下,递归会使用一个不停调用自己的函数。尽管表面上看起来很普通,但是递归可以帮助我们写出非常优雅的解决方案。对于某些问题,如果不用递归,就很难解决。
def listsum(numList):
if len(numList)==1:
return numList[0]
else:
return numList[0]+listsum(numList[1:])
在这一段代码中,有两个重要的思想值得探讨。首先,第2行检查列表是否只包含一个元素。这个检查非常重要,同时也是该函数的退出语句。对于长度为1的列表,其元素之和就是列表中的数。其次,listsum函数在第5行调用了自己!这就是我们将listsum称为递归函数的原因——递归函数会调用自己。
图4-1展示了在求解[1,3,5,7,9]之和时的一系列递归调用。我们需要将这一系列调用看作一系列简化操作。每一次递归调用都是在解决一个更小的问题,如此进行下去,直到问题本身不能再简化为止。
正如阿西莫夫提出的机器人三原则一样,所有的递归算法都要遵守三个重要的原则:
def toStr(n,base):
convertString="0123456789ABCDEF"
if n<base:
return convertString[n]
else:
return toStr(n//base,base)+convertString[n%base]
print(toStr(10,2))
来看看该算法如何将整数10转换成其对应的二进制字符串"1010"。
图4-4展示了结果,但是看上去数位的顺序反了。由于第7行首先进行递归调用,然后才拼接余数对应的字符串,因此程序能够正确工作。如果将convertstring查找和返回toStr调用反转,结果字符串就是反转的。但是将拼接操作推迟到递归调用返回之后,就能得到正确的结果。说到这里,你应该能想起第3章讨论的栈。
假设不拼接递归调用tostr 的结果和 convertstring 的查找结果,而是在进行递归调用之前把字符串压入栈中。代码清单4-4展示了修改后的实现。
from stackModule import Stack
rStack=Stack()
def toStr(n,base):
convertString="0123456789ABCDEF"
if n < base:
rStack.push( convertString[n])
else:
rStack.push(convertString[n%base])
toStr(n // base, base)
toStr(10,2)
while not rStack.isEmpty():
print(rStack.pop(),end="")
我们将使用Python的turtle模块来绘制图案。Python 的各个版本都提供turtle模块,它用起来非常简便。顾名思义,可以用turtle模块创建一只小乌龟( turtle)并让它向前或向后移动,或者左转、右转。小乌龟的尾巴可以抬起或放下。当尾巴放下时,移动的小乌龟会在其身后画出一条线。若要增加美观度,可以改变小乌龟尾巴的宽度以及尾尖所蘸墨水的颜色。
让我们通过一个简单的例子来展示小乌龟绘图的过程。使用turtle模块递归地绘制螺旋线,如代码清单4-5所示。先导人turtle模块,然后创建一个小乌龟对象,同时也会创建用于绘制图案的窗口。接下来定义drawSpiral函数。这个简单函数的基本情况是,要画的线的长度(参数len)降为0。如果线的长度大于0,就让小乌龟向前移动len个单位距离,然后向右转90度。递归发生在用缩短后的距离再一次调用drawSpiral函数时。代码清单4-5在结尾处调用了mywin.exitonclick ()函数,这使小乌龟进入等待模式,直到用户在窗口内再次点击之后,程序清理并退出。
from turtle import *
myTurtle=Turtle()
myWin=myTurtle.getscreen()
def drawSpiral(myTurtle,lineLen):
if lineLen>0:
myTurtle.forward(lineLen)
myTurtle.right(90)
drawSpiral(myTurtle,lineLen-5)
drawSpiral(myTurtle,100)
myWin.exitonclick()
from turtle import *
def tree(branchLen,t):
if branchLen>5:
t.forward(branchLen)
t.right(20)
tree(branchLen-15,t)
t.left(40)
tree(branchLen-10,t)
t.right(20)
t.backward(branchLen)
t=Turtle()
myWin=t.getscreen()
t.left(90)
t.up()
t.backward(300)
t.down()
t.color('green')
tree(110,t)
myWin.exitonclick()
from turtle import *
def drawTriangle(points,color,myTurtle):
myTurtle.fillcolor(color)
myTurtle.up()
myTurtle.goto(points[0])
myTurtle.down()
myTurtle.begin_fill()
myTurtle.goto(points[1])
myTurtle.goto(points[2])
myTurtle.goto(points[0])
myTurtle.end_fill()
def getMid(p1,p2):
return ((p1[0]+p2[0])/2,(p1[1]+p2[1])/2)
def sierpinski(points,degree,myTutle):
colormap=['blue','red','green','white','yellow','violet','orange']
drawTriangle(points,colormap[degree],myTutle)
if degree>0:
sierpinski([points[0],
getMid(points[0],points[1]),
getMid(points[0],points[2])],
degree-1,myTutle)
sierpinski([points[1],
getMid(points[0], points[1]),
getMid(points[0], points[2])],
degree - 1, myTutle)
sierpinski([points[2],
getMid(points[2], points[1]),
getMid(points[0], points[2])],
degree - 1, myTutle)
myTutle=Turtle()
myWin=myTutle.getscreen()
myPoints=[(-250,-125),(0,250),(250,-125)]
sierpinski(myPoints,3,myTutle)
myWin.exitonclick()
代码清单4-7中的程序遵循了之前描述的思想。sierpinski首先绘制外部的三角形,接着进行3个递归调用,每一个调用对应生成的一个新三角形。本例再一次使用Python自带的标准turtle模块。在 Python解释器中执行help ( ‘turtle’),可以详细了解turtle模块中的所有方法。
请根据代码清单4-7思考三角形的绘制顺序。假设三个角的顺序是左下角、顶角、右下角。由于.sierpinski 的递归调用方式,它会一直在左下角绘制三角形,直到绘制完最小的三角形才会往回绘制剩下的三角形。之后,它会开始绘制顶部的三角形,直到绘制完最小的三角形。最后,它会绘制右下角的三角形,直到全部绘制完成。
函数调用图有助于理解递归算法。由图 4-10可知,递归调用总是往左边进行的。在图中,黑线表示正在执行的函数,灰线表示没有被执行的函数。越深入到该图的底部,三角形就越小。函数一次完成一层的绘制;一旦它绘制好底层左边的三角形,就会接着绘制底层中间的三角形,依此类推。
汉诺塔
如何才能递归地解决这个问题呢?它真的可解吗?基本情况是什么?让我们自底向上地来考虑这个问题。假设第一根柱子起初有5个盘子。如果我们知道如何把上面4个盘子移动到第二根柱子上,那么就能轻易地把最底下的盘子移动到第三根柱子上,然后将4个盘子从第二根柱子移动到第三根柱子。但是如果不知道如何移动4个盘子,该怎么办呢?如果我们知道如何把上面3个盘子移动到第三根柱子上,那么就能轻易地把第4个盘子移动到第二根柱子上,然后再把3个盘子从第三根柱子移动到第二根柱子。但是如果不知道如何移动3个盘子,该怎么办呢?移动两个盘子到第二根柱子,然后把第3个盘子移动到第三根柱子,最后把之前的两个盘子移过来,怎么样?但是如果还是不知道如何移动两个盘子,该怎么办呢?你肯定会说,把一个盘子移动到第三根柱子并不难,甚至会说太简单。这看上去就是本例的基本情况。
以下概述如何借助一根中间柱子,将高度为height 的一叠盘子从起点柱子移到终点柱子:
(1)借助终点柱子,将高度为height - 1的一叠盘子移到中间柱子;
(2)将最后一个盘子移到终点柱子;
(3)借助起点柱子,将高度为height - 1的一叠盘子从中间柱子移到终点柱子。
只要总是遵守大盘子不能叠在小盘子之上的规则,就可以递归地执行上述步骤,就像最下面的大盘子不存在一样。上述步骤仅缺少对基本情况的判断。最简单的汉诺塔只有一个盘子。在这种情况下,只需将这个盘子移到终点柱子即可,这就是基本情况。此外,上述步骤通过逐渐减小高度height来向基本情况靠近
def moveTower(height,fromPole,toPole,withPole):
if height>1:
moveTower(height-1,fromPole,withPole,toPole)
moveDisk(fromPole,toPole)
moveTower(height-1,withPole,toPole,fromPole)
def moveDisk(fp,tp):
print("move disk from %d to %d"%(fp,tp))
moveTower(5,1,3,2)
本节探讨一个与蓬勃发展的机器人领域相关的问题:走出迷宫。如果你有一个Roomba扫地机器人,或许能利用在本节学到的知识对它进行重新编程。我们要解决的问题是帮助小乌龟走出虚拟的迷宫。迷宫问题源自忒修斯大战牛头怪的古希腊神话传说。相传,在迷宫里杀死牛头怪之后,忒修斯用一个线团找到了迷宫的出口。本节假设小乌龟被放置在迷宫里的某个位置,我们要做的是帮助它爬出迷宫,如图4-12所示。
该函数做的第一件事就是调用updatePosition(第2行)。这样做是为了对算法进行可视化,以便我们看到小乌龟如何在迷宫中寻找出口。接着,该函数检查前3种基本情况:是否遇到了墙(第5行)﹖是否遇到了已经走过的格子(第8行)﹖是否找到了出口(第11行)?如果没有一种情况符合,则继续递归搜索。
递归搜索调用了4个searchFrom。很难预测一共会进行多少个递归调用,这是因为它们都是用布尔运算符or连接起来的。如果第一次调用searchFrom后返回True,那么就不必进行后续的调用。可以这样理解:向北移动一格是离开迷宫的路径上的一步。如果向北没有能够走出迷宫,那么就会尝试下一个递归调用,即向南移动。如果向南失败了,就尝试向西,最后向东。如果所有的递归调用都失败了,就说明遇到了死胡同。请下载或自己输入代码,改变4个递归调用的顺序,看看结果如何。
from turtle import *
class Maze:
def __init__(self,mazeFileName):
rowsInMaze=0
columnsInMaze=0
self.mazelist=[]
mazeFile=open(mazeFileName,'r')
rowsInMaze=0
for line in mazeFile:
rowList=[]
col=0
for ch in line[:-1]:
rowList.append(ch)
if ch=='S':
self.startRow=rowsInMaze
self.startCol=col
col=col+1
rowsInMaze=rowsInMaze+1
self.mazelist.append(rowList)
columnsInMaze=len(rowList)
self.rowsInMaze=rowsInMaze
self.columnsInMaze=columnsInMaze
self.xTranslate=-columnsInMaze/2
self.yTranslate=rowsInMaze/2
self.t=Turtle(shape='turtle')
setup(width=600,height=600)
setworldcoordinates(-(columnsInMaze-1)/2-.5,
-(rowsInMaze-1)/2-.5,
(columnsInMaze-1)/2+.5,
(rowsInMaze-1)/2+.5)
class Maze:
def __init__(self,mazeFileName):
rowsInMaze=0
columnsInMaze=0
self.mazelist=[]
mazeFile=open(mazeFileName,'r')
rowsInMaze=0
for line in mazeFile:
rowList=[]
col=0
for ch in line[:-1]:
rowList.append(ch)
if ch=='S':
self.startRow=rowsInMaze
self.startCol=col
col=col+1
rowsInMaze=rowsInMaze+1
self.mazelist.append(rowList)
columnsInMaze=len(rowList)
self.rowsInMaze=rowsInMaze
self.columnsInMaze=columnsInMaze
self.xTranslate=-columnsInMaze/2
self.yTranslate=rowsInMaze/2
self.t=Turtle(shape='turtle')
setup(width=600,height=600)
setworldcoordinates(-(columnsInMaze-1)/2-.5,
-(rowsInMaze-1)/2-.5,
(columnsInMaze-1)/2+.5,
(rowsInMaze-1)/2+.5)
def drawMaze(self):
for y in range(self.rowsInMaze):
for x in range(self.columnsInMaze):
if self.mazelist[y][x]==OBSTACLE:
self.drawCenteredBox(x+self.xTranslate,
-y+self.yTranslate,
'tan')
self.t.color('black','blue')
def drawCenteredBox(self,x,y,color):
tracer(0)
self.t.up()
self.t.goto(x-.5,y-.5)
self.t.color('black','blue')
self.t.setheading(90)
self.t.down()
self.t.begin_fill()
for i in range(4):
self.t.forward(1)
self.t.right(90)
self.t.end_fill()
update()
tracer(1)
def moveTurtle(self,x,y):
self.t.up()
self.t.setheading(self.t.towards(x+self.xTranslate,
-y+self.yTranslate))
self.t.goto(x+self.xTranslate,-y+self.yTranslate)
def dropBreadcrumb(self,color):
self.t.dot(color)
def updatePosition(self,row,col,val=None):
if val:
self.mazelist[row][col]=val
self.moveTurtle(col,row)
if val==PART_OF_PATH:
color='green'
elif val==OBSTACLE:
color='red'
elif val==TRIED:
color='black'
elif val==DEAD_END:
color='red'
else :
color=None
if color:
self.dropBreadcrumb(color)
def isExit(self,row,col):
return (row==0 or
row==self.rowsInMaze-1 or
col==0 or
col ==self.columnsInMaze-1)
def __getitem__(self, idx):
return self.mazelist[idx]
def isExit(self,row,col):
return (row==0 or
row==self.rowsInMaze-1 or
col==0 or
col ==self.columnsInMaze-1)
def __getitem__(self, idx):
return self.mazelist[idx]
代码清单4-14实现了上述算法。第 3行检查是否为基本情况:尝试使用1枚硬币找零。如果没有一个硬币面值与找零金额相等,就对每一个小于找零金额的硬币面值进行递归调用。第6行使用列表循环来筛选出小于当前找零金额的硬币面值。第7行的递归调用将找零金额减去所选的硬币面值,并将所需的硬币数加1,以表示使用了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))
def recDC(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+recDC(coinValueList,change-i,knownResults)
if numCoins<minCoins:
minCoins=numCoins
knownResults[change]=minCoins
return minCoins
print(recDC([1,5,10,25],51,[0]*52))#输入的0列表必须必零钱大一,不然会发生数组越界
def dpMakeChange(coinValueList,change,minCoins):
for cents in range(change+1):
coinCount=cents
for j in [c for c in coinValueList if c<=cents]:
if minCoins[cents-j]+1<coinCount:
coinCount=minCoins[cents-j]+1
minCoins[cents]=coinCount
return minCoins[change]
def dpMakeChange(coinValueList,change,minCoins,coinUsed):
for cents in range(change+1):
coinCount=cents
newCoin=1
for j in [c for c in coinValueList if c<=cents]:
if minCoins[cents-j]+1<coinCount:
coinCount=minCoins[cents-j]+1
newCoin=j
minCoins[cents]=coinCount
coinUsed[cents]=newCoin
return minCoins[change]
def printCoins(coinsUsed,change):
coin=change
while coin>0:
thisCoin=coinsUsed[coin]
print(thisCoin)
coin=coin-thisCoin
c1=[1,5,10,21,25]
coinsUsed=[0]*53
coinCount=[0]*53
print(dpMakeChange(c1,52,coinCount,coinsUsed))
printCoins(coinsUsed,52)
print(coinsUsed)
本章探讨了递归算法的一些例子。选择这些算法,是为了让你理解递归能高效地解决何种问题。以下是本章的要点。
1.所有递归算法都必须有基本情况。
2.递归算法必须改变其状态并向基本情况靠近。递归算法必须递归地调用自己。
3.递归在某些情况下可以替代循环。
4.递归算法往往与问题的形式化表达相对应。
5.递归并非总是最佳方案。有时,递归算法比其他算法的计算复杂度更高。
存储于列表等集合中的数据项彼此存在线性或顺序的关系,每个数据项的位置与其他数据项相关。在Python列表中,数据项的位置就是它的下标。因为下标是有序的,所以能够顺序访问,由此可以进行顺序搜索。
分析顺序搜索算法
在分析搜索算法之前,需要定义计算的基本单元,这是解决此类问题的第一步。对于搜索来说,统计比较次数是有意义的。每一次比较只有两个结果:要么找到目标元素,要么没有找到。本节做了一个假设,即元素的排列是无序的。也就是说,目标元素位于每个位置的可能性都一样大。
要确定目标元素不在列表中,唯一的方法就是将它与列表中的每个元素都比较一次。如果列表中有n个元素,那么顺序搜索要经过n次比较后才能确定目标元素不在列表中。如果列表包含目标元素,分析起来更复杂。实际上有3种可能情况,最好情况是目标元素位于列表的第一个位置,即只需比较一次;最坏情况是目标元素位于最后一个位置,即需要比较n次。
前面假设列表中的元素是无序排列的,相互之间没有关联。如果元素有序排列,顺序搜索算法的效率会提高吗?
假设列表中的元素按升序排列。如果存在目标元素,那么它出现在n个位置中任意一个位置的可能性仍然一样大,因此比较次数与在无序列表中相同。不过,如果不存在目标元素,那么搜索效率就会提高。图5-2展示了算法搜索目标元素50的过程。注意,顺序搜索算法一路比较列表中的元素,直到遇到54。该元素蕴含额外的信息:54不仅不是目标元素,而且其后的元素也都不是,这是因为列表是有序的。因此,算法不需要搜完整个列表,比较完54之后便可以立即停止。代码清单5-2展示了有序列表的顺序搜索函数。
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 item<alist[midpoint]:
last=midpoint-1
else:
first=midpoint+1
return found
请注意,这个算法是分治策略的好例子。分治是指将问题分解成小问题,以某种方式解决小问题,然后整合结果,以解决最初的问题。对列表进行二分搜索时,先查看中间的元素。如果目标元素小于中间的元素,就只需要对列表的左半部分进行二分搜索。同理,如果目标元素更大,则只需对右半部分进行二分搜索。两种情况下,都是针对一个更小的列表递归调用二分搜索函数,如代码清单5-4所示。
def binarySearch(alist,item):
if len(alist)==0:
return False
else:
midpoint=len(alist)//2
if alist[midpoint] == item:
found = True
else:
if item < alist[midpoint]:
return binarySearch(alist[:midpoint],item)
else:
return binarySearch(alist[midpoint+1:], item)
def hash(aString,tablesize):
sum=0
for pos in range(len(aString)):
sum+=ord(aString[pos])
return sum%tablesize
print(hash('cat',11))
代码清单5-6使用两个列表创建HashTable类,以此实现映射抽象数据类型。其中,名为slots的列表用于存储键,名为data的列表用于存储值。两个列表中的键与值一一对应。在本节的例子中,散列表的初始大小是11。尽管初始大小可以任意指定,但选用一个素数很重要,这样做可以尽可能地提高冲突处理算法的效率。
class HashTable:
def __init__(self):
self.size=11
self.slots=[None]*self.size
self.data=[None]*self.size
在代码清单5-7中,hashfunction实现了简单的取余函数。处理冲突时,采用“加1”再散列函数的线性探测法。put函数假设,除非键已经在self.slots中,否则总是可以分配一个空槽。该函数计算初始的散列值,如果对应的槽中已有元素,就循环运行rehash函数,直到遇见一个空槽。如果槽中已有这个键.就用新值替换旧值。
class HashTable:
def __init__(self):
self.size=11
self.slots=[None]*self.size
self.data=[None]*self.size
def hashfunction(self,key,size):
return key%size
def rehash(self,oldhash,size):
return (oldhash+1)%size
def put(self,key,data):
hashvalue=self.hashfunction(key,len(self.slots))
if self.slots[hashvalue]==None:
self.slots[hashvalue]=key
self.data[hashvalue]=data
else:
if self.slots[hashvalue]==key:
self.data[hashvalue]=data#替换
else:
nextslot=self.rehash(hashvalue,len(self.slots))
while self.slots[nextslot] != None and self.slots[nextslot]!=key:
nextslot=self.rehash(nextslot,len(self.slots))
if self.slots[nextslot]==None:
self.slots[nextslot]=key
self.data[nextslot]=data
else:
self.data[nextslot]=data#替换
class HashTable:
def __init__(self):
self.size=11
self.slots=[None]*self.size
self.data=[None]*self.size
def hashfunction(self,key,size):
return key%size
def rehash(self,oldhash,size):
return (oldhash+1)%size
def put(self,key,data):
hashvalue=self.hashfunction(key,len(self.slots))
if self.slots[hashvalue]==None:
self.slots[hashvalue]=key
self.data[hashvalue]=data
else:
if self.slots[hashvalue]==key:
self.data[hashvalue]=data#替换
else:
nextslot=self.rehash(hashvalue,len(self.slots))
while self.slots[nextslot] != None and self.slots[nextslot]!=key:
nextslot=self.rehash(nextslot,len(self.slots))
if self.slots[nextslot]==None:
self.slots[nextslot]=key
self.data[nextslot]=data
else:
self.data[nextslot]=data#替换
def get(self,key):
startslot=self.hashfunction(key,len(self.slots))
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,len(self.slots))
if position==startslot:#如果再哈希到了最开始的地方,说明遍历了一圈都没有找到,说明元素不存在
stop=True
return data
def __getitem__(self, key):
return self.get(key)
def __setitem__(self, key, data):
self.put(key,data)
h=HashTable()
h[54]="cat"
h[26]="dog"
h[93]="lion"
h[17]="tiger"
h[77]="bird"
h[31]="cow"
h[44]="goat"
h[55]="pig"
h[20]="chicken"
print(h.slots)
print(h.data)
print(h[20])
h[20]="duck"
print(h[20])
print(h.data)
print(h[99])
在衡量排序过程时,最常用的指标就是总的比较次数。:其次,当元素的排列顺序不正确时,需要交换它们的位置。交换是一个耗时的操作,总的交换次数对于衡量排序算法的总体效率来说也很重要。
第二轮遍历开始时,最大值已经在正确位置上了。还剩n-1个元素需要排列,也就是说要比较n-2对。既然每一轮都将下一个最大的元素放到正确位置上,那么需要遍历的轮数就是n-l。完成n-1轮后,最小的元素必然在正确位置上,因此不必再做处理。代码清单5-9给出了完整的bubblesort函数。该函数以一个列表为参数,必要时会交换其中的元素。
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
#交换操作也可以使用同时赋值来实现
#alist[i] ,alist[i + 1]=alist[i + 1],alist[i]
冒泡排序通常被认为是效率最低的排序算法,因为在确定最终的位置前必须交换元素。“多余”的交换操作代价很大。不过,由于冒泡排序要遍历列表中未排序的部分,因此它具有其他排序算法没有的用途。特别是,如果在一轮遍历中没有发生元素交换,就可以确定列表已经有序。可以修改冒泡排序函数,使其在遇到这种情况时提前终止。对于只需要遍历几次的列表,冒泡排序可能有优势,因为它能判断出有序列表并终止排序过程。代码清单5-10实现了如上所述的修改,这种排序通常被称作短冒泡。
def shortBubbleSort(alist):
exchange=True
passnum=len(alist)-1
while passnum>0 and exchange:
exchange=False
for i in range(passnum):
if alist[i]>alist[i+1]:
exchange=True
temp=alist[i]
alist[i]=alist[i+1]
alist[i + 1]=temp
passnum=passnum-1
选择排序在冒泡排序的基础上做了改进,每次遍历列表时只做一次交换。要实现这一点,选择排序在每次遍历时寻找最大值,并在遍历完之后将它放到正确位置上。
def selectionSort(alist):
for fillslot in range(len(alist)-1,0,-1):
positionOfMax=0
for location in range(1,fillslot+1):
if alist[location]>alist[positionOfMax]:
positionOfMax=location
temp=alist[fillslot]
alist[fillslot]=alist[positionOfMax]
alist[positionOfMax]=temp
alist=[11,2,43,1,67,22,6]
selectionSort(alist)
print(alist)
def insertionSort(alist):
for index in range(1,len(alist)):
currentvalue=alist[index]
position=index
while position>0 and alist[position-1]>currentvalue:
alist[position]=alist[position-1]
position=position-1
alist[position]=currentvalue
alist=[11,2,43,1,67,22,6]
insertionSort(alist)
print(alist)
移动操作和交换操作有一个重要的不同点。总体来说,交换操作的处理时间大约是移动操作的3倍,因为后者只需进行一次赋值。在基准测试中,插入排序算法的性能很不错。
def shellsort(alist):
sublistcount=len(alist)//2
while sublistcount>0:
for startposition in range(sublistcount):
gapInsertionSort(alist,startposition,sublistcount)
print("after increments of size ",sublistcount,"the list is ",alist)
sublistcount=sublistcount//2
def gapInsertionSort(alist,start,gap):
for i in range(start+gap,len(alist),gap):
currentvalure=alist[i]
position=i
while position>=gap and alist[position-gap]>currentvalure:
alist[position]=alist[position-gap]
position=position-gap
alist[position]=currentvalure
alist=[11,2,43,1,67,22,6]
shellsort(alist)
print(alist)
现在,我们将注意力转向使用分治策略改进排序算法。要研究的第一个算法是归并排序,它是递归算法,每次将一个列表一分为二。如果列表为空或只有一个元素,那么从定义上来说它就是有序的(基本情况)。如果列表不止一个元素,就将列表一分为二,并对两部分都递归调用归并排序。当两部分都有序后,就进行归并这一基本操作。归并是指将两个较小的有序列表归并为一个有序列表的过程。图5-22a展示了示例列表被拆分后的情况,图5-22b给出了归并后的有序列表。
在代码清单5-14中,mergesort函数以处理基本情况开始。如果列表的长度小于或等于1,说明它已经是有序列表,因此不需要做额外的处理。如果长度大于1,则通过Python的切片操作得到左半部分和右半部分。要注意,列表所含元素的个数可能不是偶数。这并没有关系,因为左右子列表的长度最多相差1。
def mergeSort(alist):
print("spliting ",alist)
if len(alist)>1:
mid=len(alist)//2
lefthalf=alist[:mid]
righthalf=alist[mid:]
mergeSort(lefthalf)
mergeSort(righthalf)
i=0
j=0
k=0
while i<len(lefthalf) and j<len(righthalf):
if lefthalf[i]<righthalf[j]:
alist[k]=lefthalf[i]
i=i+1
else:
alist[k]=righthalf[j]
j=j+1
k=k+1
while i<len(lefthalf):
alist[k]=lefthalf[i]
i=i+1
k=k+1
while j<len(righthalf):
alist[k]=righthalf[j]
j=j+1
k=k+1
print("merging ",alist)
alist=[11,2,43,1,67,22,6]
mergeSort(alist)
print(alist)
在代码清单5-15中,快速排序函数 quickSort调用了递归函数 quickSortHelper。quickSortHelper首先处理和归并排序相同的基本情况。如果列表的长度小于或等于1,说明它已经是有序列表;如果长度大于1,则进行分区操作并递归地排序。分区函数 partition实现了前面描述的过程。
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 alist[rightmark]>=pivotvalue and rightmark>=leftmark:
rightmark=rightmark-1
if rightmark<leftmark:
done=True
else:
temp=alist[leftmark]
alist[leftmark]=alist[rightmark]
alist[rightmark]=temp
temp=alist[first]
alist[first]=alist[rightmark]
alist[rightmark]=temp
return rightmark
alist=[11,2,43,1,67,22,6]
quickSort(alist)
print(alist)
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,newVal):
root[0]=newVal
def getLeftChild(root):
return root[1]
def getRightChild(root):
return root[2]
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,newVal):
root[0]=newVal
def getLeftChild(root):
return root[1]
def getRightChild(root):
return root[2]
r=BinaryTree(3)
insertLeft(r,4)
print(r)
insertLeft(r,5)
print(r)
insertRight(r,6)
print(r)
insertRight(r,7)
print(r)
l=getLeftChild(r)
print(l)
setRootVal(l,9)
print(r)
insertLeft(l,11)
print(l)
print(r)
print(getRightChild(getRightChild(r)))
首先定义一个简单的类,如代码清单6-5所示。“节点与引用”表示法的要点是,属性left和right 会指向BinaryTree类的其他实例。举例来说,在向树中插入新的左子树时,我们会创建另一个BinaryTree实例,并将根节点的self.leftChild改为指向新树。
class BinaryTree:
def __init__(self,rootObj):
self.key=rootObj
self.leftChild=None
self.rightChild=None
下面看看基于根节点构建树所需要的函数。为了给树添加左子树,我们新建一个二叉树对象,将根节点的left属性指向新对象。代码清单6-6给出了insertLeft 函数的代码。
def insertLeft(self,newNode):
if self.leftChild==None:
self.leftChild=BinaryTree(newNode)
else:
t=BinaryTree(newNode)
t.left=self.leftChild
self.leftChild=t
在插入左子树时,必须考虑两种情况。第一种情况是原本没有左子节点。此时,只需往树中添加一个节点即可。第二种情况是已经存在左子节点。此时,插入一个节点,并将已有的左子节点降一层。代码清单6-6中的else语句处理的就是第二种情况。
insertRight函数也要考虑相应的两种情况:要么原本没有右子节点,要么必须在根节点和已有的右子节点之间插人一个节点。代码清单6-7给出了insertRight函数的代码。
def insertRight(self,newNode):
if self.rightChild==None:
self.rightChild=BinaryTree(newNode)
else:
t=BinaryTree(newNode)
t.right=self.rightChild
self.rightChild=t
def getRightChild(self):
return self.rightChild
def getLeftChild(self):
return self.leftChild
def setRootVal(self,obj):
self.key=obj
def getRootVal(self):
return self.key
有了创建与操作二叉树的所有代码,现在用它们来进一步了解结构。我们创建一棵简单的树,并为根节点a添加子节点 b和c。下面的Python会话创建了这棵树,并查看key, left和right中存储的值。注意,根节点的左右子节点本身都是BinaryTree类的实例。正如递归定义所言,二叉树的所有子树也都是二叉树。
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=BinaryTree(newNode)
else:
t=BinaryTree(newNode)
t.left=self.leftChild
self.leftChild=t
def insertRight(self,newNode):
if self.rightChild==None:
self.rightChild=BinaryTree(newNode)
else:
t=BinaryTree(newNode)
t.right=self.rightChild
self.rightChild=t
def getRightChild(self):
return self.rightChild
def getLeftChild(self):
return self.leftChild
def setRootVal(self,obj):
self.key=obj
def getRootVal(self):
return self.key
r=BinaryTree('a')
print(r.getRootVal())
print(r.getLeftChild())
r.insertLeft('b')
print(r.getLeftChild())
print(r.getLeftChild().getRootVal())
r.insertRight('c')
print(r.getRightChild())
print(r.getRightChild().getRootVal())
r.getRightChild().setRootVal('hello')
print(r.getRightChild().getRootVal())
树的实现已经齐全了,现在来看看如何用树解决一些实际问题。本节介绍解析树,可以用它来表示现实世界中像句子(如图6-9所示)或数学表达式这样的构造。
本例表明,在构建解析树的过程中,需要追踪当前节点及其父节点。可以通过getLeftchild与getRightchild获取子节点,但如何追踪父节点呢?一个简单的办法就是在遍历这棵树时使用栈记录父节点。每当要下沉至当前节点的子节点时,先将当前节点压到栈中。当要返回到当前节点的父节点时,就将父节点从栈中弹出来。
from stackModule import Stack
from BinaryTreeModule import BinaryTree
def bulidParseTree(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(eval(i))
parent=pStack.pop()
currentTree=parent
elif i in '+-*/':
currentTree.setRootVal(i)
currentTree.insertRight('')
pStack.push(currentTree)
currentTree=currentTree.getRightChild()
elif i==')':
currentTree=pStack.pop()
else:
raise ValueError('unknown poerator:'+i)
return eTree
from stackModule import Stack
from BinaryTreeModule import BinaryTree
import operator
def bulidParseTree(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(eval(i))
parent=pStack.pop()
currentTree=parent
elif i in '+-*/':
currentTree.setRootVal(i)
currentTree.insertRight('')
pStack.push(currentTree)
currentTree=currentTree.getRightChild()
elif i==')':
currentTree=pStack.pop()
else:
raise ValueError('unknown poerator:'+i)
return eTree
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()
假设我们从前往后阅读这本书,那么阅读顺序就符合前序遍历的次序。从根节点“书”开始,遵循前序遍历指令,对左子节点“第1章”递归调用preorder函数。然后,对“第1章”的左子节点递归调用preorder函数,得到节点“1.1节”。由于该节点没有子节点,因此不必再进行递归调用。沿着树回到节点“第1章”,接下来访问它的右子节点,即“1.2节”。和前面一样,先访问左子节点“1.2.1节”,然后访问右子节点“1.2.2节”。访问完“1.2节”之后,回到“第1章”。接下来,回到根节点,以同样的方式访问节点“第2章”。
def preorder(tree):
if tree:
print(tree.getRootVal())
preorder(tree.getLeftChild())
preorder(tree.getRightChild())
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=BinaryTree(newNode)
else:
t=BinaryTree(newNode)
t.left=self.leftChild
self.leftChild=t
def insertRight(self,newNode):
if self.rightChild==None:
self.rightChild=BinaryTree(newNode)
else:
t=BinaryTree(newNode)
t.right=self.rightChild
self.rightChild=t
def getRightChild(self):
return self.rightChild
def getLeftChild(self):
return self.leftChild
def setRootVal(self,obj):
self.key=obj
def getRootVal(self):
return self.key
def preorder(self):
print(self.key)
if self.leftChild:
self.leftChild.preorder()
if self.rightChild:
self.rightChild.preorder()
def postorder(tree):
if tree:
postorder(tree.getLeftChild())
postorder(tree.getRightChild())
print(tree.getRootVal())
import operator
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=BinaryTree(newNode)
else:
t=BinaryTree(newNode)
t.left=self.leftChild
self.leftChild=t
def insertRight(self,newNode):
if self.rightChild==None:
self.rightChild=BinaryTree(newNode)
else:
t=BinaryTree(newNode)
t.right=self.rightChild
self.rightChild=t
def getRightChild(self):
return self.rightChild
def getLeftChild(self):
return self.leftChild
def setRootVal(self,obj):
self.key=obj
def getRootVal(self):
return self.key
def preorder(self):
print(self.key)
if self.leftChild:
self.leftChild.preorder()
if self.rightChild:
self.rightChild.preorder()
def preorder(tree):
if tree:
print(tree.getRootVal())
preorder(tree.getLeftChild())
preorder(tree.getRightChild())
def postorder(tree):
if tree:
postorder(tree.getLeftChild())
postorder(tree.getRightChild())
print(tree.getRootVal())
def postordereval(tree):
opers = {'+': operator.add, '-': operator.sub,
'*': operator.mul, '/': operator.truediv}
res1=None
res2=None
if tree:
res1=postordereval(tree.getLeftChild())
res2=postordereval(tree.getRightChild())
if res1 and res2:
return opers[tree.getRootVal()](res1,res2)
else:
return tree.getRootVal()
def inorder(tree):#中序遍历
if tree !=None:
inorder(tree.getLeftChild())
print(tree.getRootVal())
inorder(tree.getRightChild())
通过中序遍历解析树,可以还原不带括号的表达式。接下来修改中序遍历算法,以得到完全括号表达式。唯一要做的修改是:在递归调用左子树前打印一个左括号,在递归调用右子树后打印一个右括号。代码清单6-16是修改后的函数。
import operator
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=BinaryTree(newNode)
else:
t=BinaryTree(newNode)
t.left=self.leftChild
self.leftChild=t
def insertRight(self,newNode):
if self.rightChild==None:
self.rightChild=BinaryTree(newNode)
else:
t=BinaryTree(newNode)
t.right=self.rightChild
self.rightChild=t
def getRightChild(self):
return self.rightChild
def getLeftChild(self):
return self.leftChild
def setRootVal(self,obj):
self.key=obj
def getRootVal(self):
return self.key
def preorder(self):
print(self.key)
if self.leftChild:
self.leftChild.preorder()
if self.rightChild:
self.rightChild.preorder()
def preorder(tree):
if tree:
print(tree.getRootVal())
preorder(tree.getLeftChild())
preorder(tree.getRightChild())
def postorder(tree):
if tree:
postorder(tree.getLeftChild())
postorder(tree.getRightChild())
print(tree.getRootVal())
def inorder(tree):#中序遍历
if tree !=None:
inorder(tree.getLeftChild())
print(tree.getRootVal())
inorder(tree.getRightChild())
def postordereval(tree):
opers = {'+': operator.add, '-': operator.sub,
'*': operator.mul, '/': operator.truediv}
res1=None
res2=None
if tree:
res1=postordereval(tree.getLeftChild())
res2=postordereval(tree.getRightChild())
if res1 and res2:
return opers[tree.getRootVal()](res1,res2)
else:
return tree.getRootVal()
def printexp(tree):#还原完全括号表达式
sVal=""
if tree:
sVal='('+printexp(tree.getLeftChild())
sVal=sVal+str(tree.getRootVal())
sVal=sVal+printexp(tree.getRightChild())+')'
return sVal
x=BinaryTree("*")
x.insertLeft('+')
l=x.getLeftChild()
l.insertLeft(4)
l.insertRight(5)
x.insertRight(7)
print(printexp(x))
print(postordereval(x))
为了使二叉堆能高效地工作,我们利用树的对数性质来表示它。你会在6.7.3节学到,为了保证对数性能,必须维持树的平衡。平衡的二叉树是指,其根节点的左右子树含有数量大致相等的节点。在实现二叉堆时,我们通过创建一棵完全二叉树来维持树的平衡。在完全二叉树中,除了最底层,其他每一层的节点都是满的。在最底层,我们从左往右填充节点。图6-14展示了完全二叉树的一个例子。
我们用来存储堆元素的方法依赖于堆的有序性。堆的有序性是指:对于堆中任意元素x及其父元素p,p都不大于x。图6-15也展示出完全二叉树具备堆的有序性。
def __init__(self):
self.heapList=[0]
self.currentSize=0
注意,将元素往上移时,其实是在新元素及其父元素之间重建堆的结构性质。此外,也保留了兄弟元素之间的堆性质。当然,如果新元素很小,需要继续往上一层交换。代码清单6-18给出了percUp方法的代码,该方法将元素一直沿着树向上移动,直到重获堆的结构性质。此时,heapList中的元素0正好能发挥重要作用。我们使用整数除法计算任意节点的父节点。就当前节点而言,父节点的下标就是当前节点的下标除以2。
def __init__(self):
self.heapList=[0]
self.currentSize=0
def percUp(self,i):
while i//2>0:
if self.heapList[i]<self.heapList[i//2]:
tmp=self.heapList[i//2]
self.heapList[i // 2]=self.heapList[i]
self.heapList[i]=tmp
i=i//2
def insert(self,k):
self.heapList.append(k)
self.currentSize=self.currentSize+1
self.percUp(self.currentSize)
正确定义insert方法后,就可以编写delMin方法。既然堆的结构性质要求根节点是树的最小元素,那么查找最小值就很简单。delMin方法的难点在于,如何在移除根节点之后重获堆的结构性质和有序性。可以分两步重建堆。
第一步,取出列表中的最后一个元素,将其移到根节点的位置。移动最后一个元素保证了堆的结构性质,但可能会破坏二叉堆的有序性。第二步,将新的根节点沿着树推到正确的位置,以重获堆的有序性。图6-17展示了将新的根节点移动到正确位置所需的一系列交换操作。
为了维持堆的有序性,只需交换根节点与它的最小子节点即可。重复节点与子节点的交换过程,直到节点比其两个子节点都小。
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 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 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
def buildHeap(self,alist):
i=len(alist)//2
self.currentSize=len(alist)
self.heapList=[0]+alist[:]
while(i>0):
self.percDown(i)
i=i-1
class Heap:
def __init__(self):
self.heapList=[0]
self.currentSize=0
def percUp(self,i):
while i//2>0:
if self.heapList[i]<self.heapList[i//2]:
tmp=self.heapList[i//2]
self.heapList[i // 2]=self.heapList[i]
self.heapList[i]=tmp
i=i//2
def insert(self,k):
self.heapList.append(k)
self.currentSize=self.currentSize+1
self.percUp(self.currentSize)
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 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 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
def buildHeap(self,alist):
i=len(alist)//2
self.currentSize=len(alist)
self.heapList=[0]+alist[:]
while(i>0):
self.percDown(i)
i=i-1
h=Heap()
h.buildHeap([9,6,5,2,3])
print(h.delMin())
print(h.delMin())
print(h.delMin())
print(h.delMin())
print(h.delMin())
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类提供了很多辅助函数,这大大地简化了BinarySearchTree类的工作。代码清单6-24是 TreeNode类的构造方法以及辅助函数。可以看到,很多辅助函数有助于根据子节点的位置(是左还是右)以及自己的子节点类型来给节点归类。
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__()
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 hasAnyChildren(self):
return self.rightChild or self.leftChild
def hasBothChildren(self):
return self.rightChild and self.leftChild
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
def put(self,key,val):
if self.root:
self._put(key,val,self.root)
else:
self.root=TreeNode(key,val)
self.size=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)
def __setitem__(self, key, value):
self.put(key,value)
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):
return self.get(key)
def __contains__(self, key):
if self._get(key,self.root):
return True
else:
return False
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 __delete__(self, key):
self.delete(key)
情况Ⅰ很简单。如果当前节点没有子节点,要做的就是删除这个节点,并移除父节点对这个节点的引用,如代码清单6-30所示。
def remove(self,currentNode):
if currentNode.isLeaf():
if currentNode==currentNode.parent.leftChild:
currentNode.parent.leftChild=None
else:
currentNode.parent.rightChild=None
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.leftChild.parent = currentNode.parent
currentNode.parent.rightChild = currentNode.leftChild
else:
currentNode.replaceNodeData(currentNode.leftChild.key,
currentNode.leftChild.payload,
currentNode.leftChild.leftChild,
currentNode.leftChild.rightChild)
else:
if currentNode.isLeftChild():
currentNode.rightChild.parent=currentNode.parent
currentNode.parent.leftChild=currentNode.rightChild
elif currentNode.isRightChild():
currentNode.rightChild.parent=currentNode.parent
currentNode.parent.rightChild=currentNode.rightChild
else:
currentNode.replaceNodeData(currentNode.rightChild.key,
currentNode.rightChild.payload,
currentNode.rightChild.leftChild,
currentNode.rightChild.rightChild)
情况3最难处理。如果一个节点有两个子节点,那就不太可能仅靠用其中一个子节点取代它来解决问题。不过,可以搜索整棵树,找到可以替换待删除节点的节点。候选节点要能为左右子树都保持二叉搜索树的关系,也就是树中具有次大键的节点。我们将这个节点称为后继节点,有一种方法能快速找到它。后继节点的子节点必定不会多于一个,所以我们知道如何按照已实现的两种删除方法来移除它。移除后继节点后,只需直接将它放到树中待删除节点的位置上即可。
处理情况3的代码如代码清单6-32所示。注意,我们用辅助函数findsuccessor和findMin来寻找后继节点,并用spliceout方法移除它(如代码清单6-34所示)。之所以用spliceout方法,是因为它可以直接访问待拼接的节点,并进行正确的修改。虽然也可以递归调用delete,但那样做会浪费时间重复搜索键的节点。
def remove(self,currentNode):
if currentNode.isLeaf():
if currentNode==currentNode.parent.leftChild:
currentNode.parent.leftChild=None
else:
currentNode.parent.rightChild=None
elif currentNode.hasBothChild():
succ=currentNode.findSuccessor()
succ.spliceOut()
currentNode.key=succ.key
currentNode.payload=succ.payload
else:#只有一个子节点
if currentNode.hasLeftChild():
if currentNode.isLeftChild():
currentNode.leftChild.parent=currentNode.parent
currentNode.parent.leftChild=currentNode.leftChild
elif currentNode.isRightChild():
currentNode.leftChild.parent = currentNode.parent
currentNode.parent.rightChild = currentNode.leftChild
else:
currentNode.replaceNodeData(currentNode.leftChild.key,
currentNode.leftChild.payload,
currentNode.leftChild.leftChild,
currentNode.leftChild.rightChild)
else:
if currentNode.isLeftChild():
currentNode.rightChild.parent=currentNode.parent
currentNode.parent.leftChild=currentNode.rightChild
elif currentNode.isRightChild():
currentNode.rightChild.parent=currentNode.parent
currentNode.parent.rightChild=currentNode.rightChild
else:
currentNode.replaceNodeData(currentNode.rightChild.key,
currentNode.rightChild.payload,
currentNode.rightChild.leftChild,
currentNode.rightChild.rightChild)
def findSuccessor(self):
succ=None
if self.hasRightChild():
succ=self.rightChild.findMin()
else:
if self.parent:
if self.isLeftChild():
succ=self.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
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.leftChild
self.leftChild.parent=self.parent
else:
if self.isLeftChild():
self.parent.leftChild=self.rightChild
else:
self.parent.rightChild=self.rightChild
self.rightChild.parent=self.parent
def __iter__(self):
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
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__()
def put(self,key,val):
if self.root:
self._put(key,val,self.root)
else:
self.root=TreeNode(key,val)
self.size=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)
def __setitem__(self, key, value):
self.put(key,value)
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):
return self.get(key)
def __contains__(self, key):
if self._get(key,self.root):
return True
else:
return False
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 remove(self,currentNode):
if currentNode.isLeaf():#叶子节点
if currentNode==currentNode.parent.leftChild:
currentNode.parent.leftChild=None
else:
currentNode.parent.rightChild=None
elif currentNode.hasBothChild():#内部
succ=currentNode.findSuccessor()
succ.spliceOut()
currentNode.key=succ.key
currentNode.payload=succ.payload
else:#只有一个子节点
if currentNode.hasLeftChild():
if currentNode.isLeftChild():
currentNode.leftChild.parent=currentNode.parent
currentNode.parent.leftChild=currentNode.leftChild
elif currentNode.isRightChild():
currentNode.leftChild.parent = currentNode.parent
currentNode.parent.rightChild = currentNode.leftChild
else:
currentNode.replaceNodeData(currentNode.leftChild.key,
currentNode.leftChild.payload,
currentNode.leftChild.leftChild,
currentNode.leftChild.rightChild)
else:
if currentNode.isLeftChild():
currentNode.rightChild.parent=currentNode.parent
currentNode.parent.leftChild=currentNode.rightChild
elif currentNode.isRightChild():
currentNode.rightChild.parent=currentNode.parent
currentNode.parent.rightChild=currentNode.rightChild
else:
currentNode.replaceNodeData(currentNode.rightChild.key,
currentNode.rightChild.payload,
currentNode.rightChild.leftChild,
currentNode.rightChild.rightChild)
def __delete__(self, key):
self.delete(key)
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 hasAnyChildren(self):
return self.rightChild or self.leftChild
def hasBothChildren(self):
return self.rightChild and self.leftChild
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
def findSuccessor(self):
succ=None
if self.hasRightChild():
succ=self.rightChild.findMin()
else:
if self.parent:
if self.isLeftChild():
succ=self.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
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.leftChild
self.leftChild.parent=self.parent
else:
if self.isLeftChild():
self.parent.leftChild=self.rightChild
else:
self.parent.rightChild=self.rightChild
self.rightChild.parent=self.parent
def __iter__(self):
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
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)
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)
def updateBalance(self,node):
if node.balanceFactor>1 or node.balanceFactor<-1:
self.rebalance(node)
return
if node.parent!=None:
if node.isLeftChild():
node.parent.balanceFactor+=1
elif node.isRightChild():
node.parent.balanceFactor-=1
if node.parent.balanceFactor!=0:
self.updateBalance(node.parent)
新方法updateBalance做了大部分工作,它实现了前面描述的递归过程。updateBalance方法先检查当前节点是否需要再平衡(第18行)。如果符合判断条件,就进行再平衡,不需要更新父节点;如果当前节点不需要再平衡,就调整父节点的平衡因子。如果父节点的平衡因子非零,那么沿着树往根节点的方向递归调用updateBalance方法。
如果需要进行再平衡,该怎么做呢?高效的再平衡是让AVL树发挥作用同时不损性能的关键。为了让AVL树恢复平衡,需要在树上进行一次或多次旋转。
def rotateLeft(self,rotRoot):
newRoot=rotRoot.rightChild
rotRoot.rightChild=newRoot.leftChild
if newRoot.leftChild!=None:
newRoot.leftChild.parent=rotRoot
newRoot.parent=rotRoot.parent
if rotRoot.isRoot():
self.root=newRoot
else:
if rotRoot.isLeftChild():
rotRoot.parent.leftChild=newRoot
else:
rotRoot.parent.rightChild=newRoot
newRoot.leftChild=rotRoot
rotRoot.parent=newRoot
rotRoot.balanceFactor=rotRoot.balanceFactor+1-min(newRoot.balanceFactor,0)
newRoot.balanceFactor=newRoot.balanceFactor+1-max(rotRoot.balanceFactor,0)
def rebalance(self,node):
if node.balanceFactor<0:
if node.rightChild.balanceFactor>0:
self.rotateRight(node.rightChild)
self.rotateLeft(node)
else:
self.rotateLeft(node)
elif node.balanceFactor>0:
if node.leftChild.balanceFactor<0:
self.rotateLeft(node.leftChild)
self.rotateRight(node)
else:
self.rotateRight(node)
本章和第5章介绍了可以用来实现映射这一抽象数据类型的多种数据结构,包括有序列表、散列表、二叉搜索树以及AVL树。表6-1总结了每个数据结构的性能。
class Vertex:
def __init__(self,key):
self.id=key
self.connectedTo={}
def addNeighbor(self,nbr,weight=0):
self.connectedTo[nbr]=weight
def __str__(self):
return str(self.id)+" connectedTo: "+str([x.id for x in self.connectedTo])
def getConnections(self):
return self.connectedTo.keys()
def getId(self):
return self.id
def getWeight(self,nbr):
return self.connectedTo[nbr]
Graph类的实现如代码清单7-2所示,其中包含一个将顶点名映射到顶点对象的字典。在图7-4中,该字典对象由灰色方块表示。Graph类也提供了向图中添加顶点和连接不同顶点的方法。getVertices方法返回图中所有顶点的名字。此外,我们还实现了_iter__方法,从而使遍历图中的所有顶点对象更加方便。总之,这两个方法使我们能够根据顶点名或者顶点对象本身遍历图中的所有顶点。
class Graph:
def __init__(self):
self.vertList={}#字典里面存放 顶点:顶点对象
self.numVertices=0#顶点个数
def addVertex(self,key):#向图中添加新顶点
self.numVertices=self.numVertices+1
newVertex=Vertex(key)#构造vertex对象,添加新顶点
self.vertList[key]=newVertex#将顶点名映射到顶点对象
return newVertex
def getVertex(self,n):
if n in self.vertList:
return self.vertList[n]
else:
return None
def __contains__(self, n):
return n in self.vertList
def addEdge(self,f,t,cost=0):
if f not in self.vertList:#如果起点不在vertList里面,则把起点添加进vertList
nv=self.addVertex(f)
if t not in self.vertList:
nv = self.addVertex(t)#如果终点不在vertList里面,则把终点添加进vertList
#调用Vertex类的addNeighbor方法把边连接上
self.vertList[f].addNeighbor(self.vertList[t],cost)
def getVertices(self):
return self.vertList.keys()
def __iter__(self):
#返回迭代器
return iter(self.vertList.values())
class Vertex:
def __init__(self,key):
self.id=key
self.connectedTo={}
def addNeighbor(self,nbr,weight=0):#注意nbr是对象
self.connectedTo[nbr]=weight
def __str__(self):
return str(self.id)+" connectedTo: "+str([x.id for x in self.connectedTo])
def getConnections(self):
return self.connectedTo.keys()#keys里面存放的是对象,原因在于addEdge(0,1,5)语句
def getId(self):
return self.id
def getWeight(self,nbr):
return self.connectedTo[nbr]
class Graph:
def __init__(self):
self.vertList={}#字典里面存放 顶点:顶点对象
self.numVertices=0#顶点个数
def addVertex(self,key):#向图中添加新顶点
self.numVertices=self.numVertices+1
newVertex=Vertex(key)#构造vertex对象,添加新顶点
self.vertList[key]=newVertex#将顶点名映射到顶点对象
return newVertex
def getVertex(self,n):
if n in self.vertList:
return self.vertList[n]
else:
return None
def __contains__(self, n):
return n in self.vertList
def addEdge(self,f,t,cost=0):
if f not in self.vertList:#如果起点不在vertList里面,则把起点添加进vertList
nv=self.addVertex(f)
if t not in self.vertList:
nv = self.addVertex(t)#如果终点不在vertList里面,则把终点添加进vertList
#调用Vertex类的addNeighbor方法把边连接上
self.vertList[f].addNeighbor(self.vertList[t],cost)
def getVertices(self):
return self.vertList.keys()
def __iter__(self):
#返回迭代器
return iter(self.vertList.values())
#ver={'v0':'v1','v0':'v2','v2':'v3'}
g=Graph()
for i in range(6):
g.addVertex(i)
print(g.vertList)
g.addEdge(0,1,5)
g.addEdge(0,5,2)
g.addEdge(1,2,4)
g.addEdge(2,3,9)
g.addEdge(3,4,7)
g.addEdge(3,5,3)
g.addEdge(4,0,1)
g.addEdge(5,4,8)
g.addEdge(5,2,1)
for v in g:#v是一个个顶点对象,使用for循环时会执行__iter__函数
# print(v)
for w in v.getConnections():
print("(%s,%s)"%(v.getId(),w.getId()))
我们从词梯问题开始学习图算法。考虑这样一个任务:将单词FOOL转换成SAGE。在解决词梯问题时,必须每次只替换一个字母,并且每一步的结果都必须是一个单词,而不能是不存在的词。词梯问题由《爱丽丝梦游仙境》的作者刘易斯·卡罗尔于1878年提出。下面的单词转换序列是样例问题的一个解。
第一个问题是如何用图来表示大的单词集合。如果两个单词的区别仅在于有一个不同的字母,就用一条边将它们相连。如果能创建这样一个图,那么其中的任意一条连接两个单词的路径就是词梯问题的一个解。图7-5展示了一个小型图,可用于解决从FOOL到SAGE 的词梯问题。注意,它是无向图,并且边没有权重。
创建这个图有多种方式。假设有一个单词列表,其中每个单词的长度都相同。首先,为每个单词创建顶点。为了连接这些顶点,可以将每个单词与列表中的其他所有单词进行比较。如果两个单词只相差一个字母,就可以在图中创建一条边,将它们连接起来。对于只有少量单词的情况,这个算法还不错。但是,假设列表中有5110个单词,将一个单词与列表中的其他所有单词进行
在 Python中,可以通过字典来实现上述方法。字典的键就是桶上的标签,值就是对应的单词列表。一旦构建好字典,就能利用它来创建图。首先为每个单词创建顶点,然后在字典中对应同一个键的单词之间创建边。代码清单7-3展示了构建图所需的Python代码。
from vertexAndGraph import Graph
def buildGraph(wordFile):
d={}
g=Graph()
wfile=open(wordFile,'r')
#创建词桶
for line in wfile:
word=line[:-1]
for i in range(len(word)):
bucket=word[:i]+'_'+word[i+1:]
if bucket in d:
d[bucket].append(word)#向建映射的列表里面增加新值
else:
d[bucket]=[word]
#为同一个桶中的单词添加顶点和边
for bucket in d.keys():#遍历d中的键,一个个键就是桶
for word1 in d[bucket]:#遍历键映射的值,值是一个列表,里面存放单词
for word2 in d[bucket]:
if word1!=word2:
g.addEdge(word1,word2)
return g
buildGraph('wordFile')
wordFile文件和代码7-3在同一文件夹下,wordFile的内容如下:
POPE
ROPE
NOPE
HOPE
LOPE
MOPE
COPE
PIPE
PAPE
POLE
PORE
POSE
POKE
POPS
这是我们在本节中遇到的第一个实际的图问题,你可能会好奇这个图的稀疏程度如何。本例中的单词列表包含5110个单词。如果使用邻接矩阵表示,就会有26112100个单元格(5110*5110 = 26112 100 )。用buildGraph函数创建的图一共有53286条边。因此,只有0.2%的单元格被填充。这显然是一个非常稀疏的矩阵。
算法执行过程比较复杂可进入bfs函数设断点进行调试,并查看执行过程,代码中构建的图是图7-9(b)用的图
class Queue:
def __init__(self):
self.items=[]
def isEmpty(self):
return self.items==[]
def enqueue(self,item):
self.items.insert(0,item)
def dequeue(self):
return self.items.pop()
def size(self):
return len(self.items)
class Vertex:
def __init__(self, key):
self.id = key
self.connectedTo = {}
self.distance=0
self.predecessor=None
self.color='white'
def setDistance(self,val):
self.distance=val
def setPred(self,val):
self.predecessor=val
def setColor(self,val):
self.color=val
def getDistance(self):
return self.distance
def getPred(self):
return self.predecessor
def getColor(self):
return self.color
def addNeighbor(self, nbr, weight=0): # 注意nbr是对象
self.connectedTo[nbr] = weight
def __str__(self):
return str(self.id) + " connectedTo: " + str([x.id for x in self.connectedTo])
def getConnections(self):
return self.connectedTo.keys() # keys里面存放的是对象,原因在于addEdge(0,1,5)语句
def getId(self):
return self.id
def getWeight(self, nbr):
return self.connectedTo[nbr]
class Graph:
def __init__(self):
self.vertList={}#字典里面存放 顶点:顶点对象
self.numVertices=0#顶点个数
def addVertex(self,key):#向图中添加新顶点
self.numVertices=self.numVertices+1
newVertex=Vertex(key)#构造vertex对象,添加新顶点
self.vertList[key]=newVertex#将顶点名映射到顶点对象
return newVertex
def getVertex(self,n):
if n in self.vertList:
return self.vertList[n]
else:
return None
def __contains__(self, n):
return n in self.vertList
def addEdge(self,f,t,cost=0):
if f not in self.vertList:#如果起点不在vertList里面,则把起点添加进vertList
nv=self.addVertex(f)
if t not in self.vertList:
nv = self.addVertex(t)#如果终点不在vertList里面,则把终点添加进vertList
#调用Vertex类的addNeighbor方法把边连接上
self.vertList[f].addNeighbor(self.vertList[t],cost)
def getVertices(self):
return self.vertList.keys()
def __iter__(self):
#返回迭代器
return iter(self.vertList.values())
def bfs(g,start):
start.setDistance(0)
start.setPred(None)
vertQueue=Queue()
vertQueue.enqueue(start)
while (vertQueue.size()>0):
currentVert=vertQueue.dequeue()
for nbr in currentVert.getConnections():#拿到当前顶点的临接顶点对象
if (nbr.getColor()=='white'):
nbr.setDistance(currentVert.getDistance()+1)
nbr.setPred(currentVert)
#把邻接顶点和当前顶点的距离和关系指明后,把邻接顶点放进队列里面方便后续出列
vertQueue.enqueue(nbr)
currentVert.setColor('black')
print(currentVert.getId())#打印宽度优先搜索
g=Graph()
g.addVertex('fool')
g.addVertex('pool')
g.addVertex('foil')
g.addVertex('foul')
g.addVertex('cool')
g.addVertex('poll')
g.addVertex('fail')
g.addVertex('pole')
g.addVertex('pall')
g.addVertex('pope')
g.addVertex('pale')
g.addVertex('page')
g.addVertex('sale')
g.addVertex('sage')
g.addEdge('fool','pool')
g.addEdge('fool','foil')
g.addEdge('fool','foul')
g.addEdge('fool','cool')
g.addEdge('pool','poll')
g.addEdge('foil','fail')
g.addEdge('poll','pole')
g.addEdge('poll','pall')
g.addEdge('pole','pope')
g.addEdge('pole','pale')
g.addEdge('pale','page')
g.addEdge('pale','sale')
g.addEdge('page','sage')
bfs(g,g.vertList['fool'])
除此以外,BFS还使用了vertex类的扩展版本。这个新的vertex类新增了3个实例变量:distance、predecessor和 color。每一个变量都有对应的getter方法和 setter方法。扩展后的vertex类被包含在pythonds包中。因为其中没有新的知识点,所以此处不展示这个类。
class Queue:
def __init__(self):
self.items=[]
def isEmpty(self):
return self.items==[]
def enqueue(self,item):
self.items.insert(0,item)
def dequeue(self):
return self.items.pop()
def size(self):
return len(self.items)
class Vertex:
def __init__(self, key):
self.id = key
self.connectedTo = {}
self.distance=0
self.predecessor=None
self.color='white'
def setDistance(self,val):
self.distance=val
def setPred(self,val):
self.predecessor=val
def setColor(self,val):
self.color=val
def getDistance(self):
return self.distance
def getPred(self):
return self.predecessor
def getColor(self):
return self.color
def addNeighbor(self, nbr, weight=0): # 注意nbr是对象
self.connectedTo[nbr] = weight
def __str__(self):
return str(self.id) + " connectedTo: " + str([x.id for x in self.connectedTo])
def getConnections(self):
return self.connectedTo.keys() # keys里面存放的是对象,原因在于addEdge(0,1,5)语句
def getId(self):
return self.id
def getWeight(self, nbr):
return self.connectedTo[nbr]
class Graph:
def __init__(self):
self.vertList={}#字典里面存放 顶点:顶点对象
self.numVertices=0#顶点个数
def addVertex(self,key):#向图中添加新顶点
self.numVertices=self.numVertices+1
newVertex=Vertex(key)#构造vertex对象,添加新顶点
self.vertList[key]=newVertex#将顶点名映射到顶点对象
return newVertex
def getVertex(self,n):
if n in self.vertList:
return self.vertList[n]
else:
return None
def __contains__(self, n):
return n in self.vertList
def addEdge(self,f,t,cost=0):
if f not in self.vertList:#如果起点不在vertList里面,则把起点添加进vertList
nv=self.addVertex(f)
if t not in self.vertList:
nv = self.addVertex(t)#如果终点不在vertList里面,则把终点添加进vertList
#调用Vertex类的addNeighbor方法把边连接上
self.vertList[f].addNeighbor(self.vertList[t],cost)
def getVertices(self):
return self.vertList.keys()
def __iter__(self):
#返回迭代器
return iter(self.vertList.values())
def bfs(g,start):
start.setDistance(0)
start.setPred(None)
vertQueue=Queue()
vertQueue.enqueue(start)
while (vertQueue.size()>0):
currentVert=vertQueue.dequeue()
for nbr in currentVert.getConnections():#拿到当前顶点的临接顶点对象
if (nbr.getColor()=='white'):
nbr.setDistance(currentVert.getDistance()+1)
nbr.setPred(currentVert)
#把邻接顶点和当前顶点的距离和关系指明后,把邻接顶点放进队列里面方便后续出列
vertQueue.enqueue(nbr)
currentVert.setColor('black')
print(currentVert.getId())#打印宽度优先搜索
def traverse(y):#回溯
x=y
while (x.getPred()):
print(x.getId())
x=x.getPred()
print(x.getId())
g=Graph()
g.addVertex('fool')
g.addVertex('pool')
g.addVertex('foil')
g.addVertex('foul')
g.addVertex('cool')
g.addVertex('poll')
g.addVertex('fail')
g.addVertex('pole')
g.addVertex('pall')
g.addVertex('pope')
g.addVertex('pale')
g.addVertex('page')
g.addVertex('sale')
g.addVertex('sage')
g.addEdge('fool','pool')
g.addEdge('fool','foil')
g.addEdge('fool','foul')
g.addEdge('fool','cool')
g.addEdge('pool','poll')
g.addEdge('foil','fail')
g.addEdge('poll','pole')
g.addEdge('poll','pall')
g.addEdge('pole','pope')
g.addEdge('pole','pale')
g.addEdge('pale','page')
g.addEdge('pale','sale')
g.addEdge('page','sage')
bfs(g,g.vertList['fool'])
print('打印回溯')
traverse(g.getVertex('sage'))
在学习其他图算法之前,让我们先分析 BFS的性能。在代码清单7-4中,第8行的 while循环对于7中的任一顶点最多只执行一次。这是因为只有白色顶点才能被访问并添加到队列中。这使得while循环的时间复杂度是O(V)。至于嵌套在while 循环中的for循环(第10行),它对每一条边都最多只会执行一次。原因是,每一个顶点最多只会出列一次,并且我们只有在顶点u出列时才会访问从u到v的边。这使得 for循环的时间复杂度为O(E)。因此,两个循环总的时间复杂度就是O(V+E)。
进行宽度优先搜索只是整个任务的一部分,从起点一直找到终点则是任务的另一部分。这部分的最坏情况是整个图是一条长链。在这种情况下,遍历所有顶点的时间复杂度是o(V)。正常情况下,时间复杂度等于O(V)乘以某个小数,但是我们仍然用O(V)来表示。
为了用图表示骑士周游问题,我们将棋盘上的每一格表示为一个顶点,同时将骑士的每一次合理走法表示为一条边。图7-10展示了骑士的合理走法以及在图中对应的边。
可以用代码清单7-6中的 Python函数来构建n×n棋盘对应的完整图。knightGraph 函数将整个棋盘遍历了一遍。当它访问棋盘上的每一格时,都会调用辅助函数genLegalNoves来创建一个列表,用于记录从这一格开始的所有合理走法。之后,所有的合理走法都被转换成图中的边。另一个辅助函数posToNodeId将棋盘上的行列位置转换成与图7-10中顶点编号相似的线性顶点数。
class Graph:
def __init__(self):
self.vertList={}#字典里面存放 顶点:顶点对象
self.numVertices=0#顶点个数
def addVertex(self,key):#向图中添加新顶点
self.numVertices=self.numVertices+1
newVertex=Vertex(key)#构造vertex对象,添加新顶点
self.vertList[key]=newVertex#将顶点名映射到顶点对象
return newVertex
def getVertex(self,n):
if n in self.vertList:
return self.vertList[n]
else:
return None
def __contains__(self, n):
return n in self.vertList
def addEdge(self,f,t,cost=0):
if f not in self.vertList:#如果起点不在vertList里面,则把起点添加进vertList
nv=self.addVertex(f)
if t not in self.vertList:
nv = self.addVertex(t)#如果终点不在vertList里面,则把终点添加进vertList
#调用Vertex类的addNeighbor方法把边连接上
self.vertList[f].addNeighbor(self.vertList[t],cost)
def getVertices(self):
return self.vertList.keys()
def __iter__(self):
#返回迭代器
return iter(self.vertList.values())
def knightGraph(bdSize):#遍历整个棋盘
ktGraph=Graph()
for row in range(bdSize):
for col in range(bdSize):
nodeId=posToNodeId(row,col,bdSize)
newPositions=genLegalMoves(row,col,bdSize)#创建一个列表,用于记录从这一格开始的所有合理走法
for e in newPositions:
nid=posToNodeId(e[0],e[1])#将棋盘上的行列位置转换成与图7-10中顶点编号相似的线性顶点数。
ktGraph.addEdge(nodeId,nid)
return ktGraph
在代码清单7-7中,genLegalMoves函数接受骑士在棋盘上的位置,并且生成8种可能的走法。legalCoord辅助函数确认走法是合理的。
def genLegalMoves(x,y,bdSize):#生成8种可能的走法
newMoves=[]
moveOffsets=[(-1,-2),(-1,2),(-2,-1),(-2,1),(1,-2),(1,2),(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
def legalCoord(x,bdSize):#确认走法是否合理
if x>=0 and x<bdSize:
return True
else:
return False
图7-11展示了在8×8的棋盘上所有合理走法所对应的完整图,其中一共有336条边。注意,与棋盘中间的顶点相比,边缘顶点的连接更少。可以看到,这个图也是非常稀疏的。如果图是完全相连的,那么会有4096条边。由于本图只有336条边,因此邻接矩阵的填充率只有8.2%。
def knightTour(n,path,u,limit):
u.setColor('gray')
path.append(u)
if n<limit:
nbrList=list(u.getConnections())
i=0
done=False
while i<len(nbrList) and not done:
if nbrList[i].getColor()=='white':
done=knightTour(n+1,path,nbrList[i],limit)
i=i+1
if not done:#准备回溯
path.pop()
u.setColor('white')
else:
done=True
return done
让我们通过一个例子来看看knightTour的运行情况,可以参照图7-12来追踪搜索的变化。这个例子假设在代码清单7-8中第6行对getConnections方法的调用将顶点按照字母顺序排好。首先调用knightTour(0, path, A,6)。
knightTour 函数从顶点A开始访问。与A相邻的顶点是B和D。按照字母顺序,B在D之前,因此 DFS选择B作为下一个要访问的顶点,如图7-12b所示。对B的访问从递归调用knightTour开始。B与C和D相邻,因此 knightTour接下来会访问C。但是,C没有白色的相邻顶点(如图7-12c所示),因此是死路。此时,将C的颜色改回白色。knightTour的调用返回 False,也就是将搜索回溯到顶点B,如图7-12d所示。接下来要访问的顶点是D,因此knightTour进行了一次递归调用来访问它。从顶点D开始,knightTour可以继续进行递归调用,直到再一次访问顶点C。但是,这一次,检验条件n < limit 失败了,因此我们知道遍历完了图中所有的顶点。此时返回True,以表明对图进行了一次成功的遍历。当返回列表时, path包含[A,B,D,E,F,C]。其中的顺序就是每个顶点只访问一次所需的顺序。
def orderByAvail(n):
resList=[]
for v in n.getConnections():
if v.getColor()=='white':
c=0
for w in v.getConnections():
if w.getColor=='white':
c=c+1
resList.append((c,v))
resList.sort(key=lambda x:x[0])
return [y[1] for y in resList]
选择合理走法最多的顶点作为下一个访问顶点的问题在于,它会使骑士在周游的前期就访问位于棋盘中间的格子。当这种情况发生时,骑士很容易被困在棋盘的一边,而无法到达另一边的那些没访问过的格子。首先访问合理走法最少的顶点,则可使骑士优先访问棋盘边缘的格子。这样做保证了骑士能够尽早访问难以到达的角落,并且在需要的时候通过中间的格子跨越到棋盘的另一边。我们称利用这类知识来加速算法为启发式技术。人类每天都在使用启发式技术做决定,启发式搜索也经常被用于人工智能领域。本例用到的启发式技术被称作Warnsdorff算法,以纪念在1823年提出该算法的数学家H.C. Warnsdorff。
class Graph:
def __init__(self):
self.vertList={}#字典里面存放 顶点:顶点对象
self.numVertices=0#顶点个数
def addVertex(self,key):#向图中添加新顶点
self.numVertices=self.numVertices+1
newVertex=Vertex(key)#构造vertex对象,添加新顶点
self.vertList[key]=newVertex#将顶点名映射到顶点对象
return newVertex
def getVertex(self,n):
if n in self.vertList:
return self.vertList[n]
else:
return None
def __contains__(self, n):
return n in self.vertList
def addEdge(self,f,t,cost=0):
if f not in self.vertList:#如果起点不在vertList里面,则把起点添加进vertList
nv=self.addVertex(f)
if t not in self.vertList:
nv = self.addVertex(t)#如果终点不在vertList里面,则把终点添加进vertList
#调用Vertex类的addNeighbor方法把边连接上
self.vertList[f].addNeighbor(self.vertList[t],cost)
def getVertices(self):
return self.vertList.keys()
def __iter__(self):
#返回迭代器
return iter(self.vertList.values())
class Vertex:
def __init__(self, key):
self.id = key
self.connectedTo = {}
self.distance=0
self.predecessor=None
self.color='white'
def setDistance(self,val):
self.distance=val
def setPred(self,val):
self.predecessor=val
def setColor(self,val):
self.color=val
def getDistance(self):
return self.distance
def getPred(self):
return self.predecessor
def getColor(self):
return self.color
def addNeighbor(self, nbr, weight=0): # 注意nbr是对象
self.connectedTo[nbr] = weight
def __str__(self):
return str(self.id) + " connectedTo: " + str([x.id for x in self.connectedTo])
def getConnections(self):
return self.connectedTo.keys() # keys里面存放的是对象,原因在于addEdge(0,1,5)语句
def getId(self):
return self.id
def getWeight(self, nbr):
return self.connectedTo[nbr]
def knightGraph(bdSize):#遍历整个棋盘
ktGraph=Graph()
for row in range(bdSize):
for col in range(bdSize):
nodeId=posToNodeId(row,col,bdSize)
newPositions=genLegalMoves(row,col,bdSize)#创建一个列表,用于记录从这一格开始的所有合理走法
for e in newPositions:
nid=posToNodeId(e[0],e[1])#将棋盘上的行列位置转换成与图7-10中顶点编号相似的线性顶点数。
ktGraph.addEdge(nodeId,nid)
return ktGraph
def genLegalMoves(x,y,bdSize):#生成8种可能的走法
newMoves=[]
moveOffsets=[(-1,-2),(-1,2),(-2,-1),(-2,1),(1,-2),(1,2),(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
def legalCoord(x,bdSize):#确认走法是否合理
if x>=0 and x<bdSize:
return True
else:
return False
# n是搜索树的当前深度;path是到当前为止访问过的顶点列表;u是希望在图中访问的顶点;limit是路径上的顶点总数。
def knightTour(n,path,u,limit):
u.setColor('gray')
path.append(u)
if n<limit:
nbrList=list(u.getConnections())
i=0
done=False
while i<len(nbrList) and not done:
if nbrList[i].getColor()=='white':
done=knightTour(n+1,path,nbrList[i],limit)
i=i+1
if not done:#准备回溯
path.pop()
u.setColor('white')
else:
done=True
return done
def orderByAvail(n):
resList=[]
for v in n.getConnections():
if v.getColor()=='white':
c=0
for w in v.getConnections():
if w.getColor=='white':
c=c+1
resList.append((c,v))
resList.sort(key=lambda x:x[0])
return [y[1] for y in resList]
骑士周游是深度优先搜索的一种特殊情况,它需要创建没有分支的最深深度优先搜索树。通用的深度优先搜索其实更简单,它的目标是尽可能深地搜索,尽可能多地连接图中的顶点,并且在需要的时候进行分支。
一次深度优先搜索甚至能够创建多棵深度优先搜索树,我们称之为深度优先森林。和宽度优先搜索类似,深度优先搜索也利用前驱连接来构建树。此外,深度优先搜索还会使用vertex类中的两个额外的实例变量:发现时间记录算法在第一次访问顶点时的步数,结束时间记录算法在顶点被标记为黑色时的步数。在学习之后会发现,顶点的发现时间和结束时间提供了一些有趣的特性,后续算法会用到这些特性。
深度优先搜索的实现如代码清单7-10所示。由于dfs函数和dfsvisit辅助函数使用一个变量来记录调用dfsvisit 的时间,因此我们选择将代码作为Graph类的一个子类中的方法来实现。该实现继承Graph类,并且增加了time实例变量,以及dfs和 dfsvisit两个方法。注意第11行,dfs方法遍历图中所有的顶点,并对白色顶点调用dfsvisit方法。之所以遍历所有的顶点,而不是简单地从一个指定的顶点开始搜索,是因为这样做能够确保深度优先森林中的所有顶点都在考虑范围内,而不会有被遗漏的顶点。for avertex in self这条语句可能看上去不太正确,但是此处的self是DFSGraph类的一个实例,遍历一个图实例中的所有顶点其实是一件非常自然的事情。
from vertexAndGraph 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实现只对回到起点的路径上的顶点感兴趣,但也可以创建一个表示图中所有顶点间的最短路径的宽度优先森林。这个问题留作练习。在接下来的两个例子中,我们会看到为何记录深度优先森林十分重要。
从startvertex开始,dfsvisit方法尽可能深地探索所有相邻的白色顶点。如果仔细观察dfsvisit 的代码并且将其与bfs 比较,应该注意到二者几乎一样,除了内部for循环的最后一行,dfsvisit通过递归地调用自己来继续进行下一层的搜索, bfs则将顶点添加到队列中,以供后续搜索。有趣的是,bfs使用队列,dfsvisit则使用栈。我们没有在代码中看到栈,但是它其实隐式地存在于dfsvisit的递归调用中。
图7-16展示了在小型图上应用深度优先搜索算法的过程。图中,虚线表示被检查过的边,但是其一端的顶点已经被添加到深度优先搜索树中。在代码中,这是通过检查另一端的顶点是否不为白色来完成的。
搜索从图中的顶点A开始。由于所有顶点一开始都是白色的,因此算法会访问A。访问顶点的第一步是将其颜色设置为灰色,以表明正在访问该顶点,并将其发现时间设为1。由于A有两个相邻顶点(B和D),因此它们都需要被访问。我们按照字母顺序来访问顶点。
接下来访问顶点B,将它的颜色设置为灰色,并把发现时间设置为2。B也与两个顶点((C和D)相邻,因此根据字母顺序访问C。
访问C时,搜索到达某个分支的终点。在将C标为灰色并且把发现时间设置为3之后,算法发现C没有相邻顶点。这意味着对C的探索完成,因此将它标为黑色,并将完成时间设置为4。图7-16d展示了搜索至这一步时的状态。
由于C是一个分支的终点,因此需要返回到B,并且继续探索其余的相邻顶点。唯一的待探索顶点就是D,它把搜索引到E。E有两个相邻顶点,即B和F。正常情况下,应该按照字母顺序来访问这两个顶点,但是由于B已经被标记为灰色,因此算法自知不应该访问B,因为如果这
么做就会陷入死循环。因此,探索过程跳过B,继续访问F。
F只有C这一个相邻顶点,但是C已经被标记为黑色,因此没有后续顶点需要探索,也即到达另一个分支的终点。从此时起,算法一路回溯到起点,同时为各个顶点设置完成时间并将它们标记为黑色,如图7-16h~图 7-161所示。
每个顶点的发现时间和结束时间都体现了括号特性,这意味着深度优先搜索树中的任一节点的子节点都有比该节点更晚的发现时间和更早的结束时间。图7-17展示了通过深度优先搜索算法构建的树。
一般来说,深度优先搜索的运行时间如下。在代码清单7-10中,若不计dfsvisit 的运行时间,第8行和第11行的循环为O(V),这是由于它们针对图中的每个顶点都只执行一次。在dfsvisit 中,第19行的循环针对当前顶点的邻接表中的每一条边都执行一次。由于dfsvisit只有在顶点是白色时被递归调用,因此循环最多会对图中的每一条边执行一次,也就是O(E)。因此,深度优先搜索算法的时间复杂度是O(V+E)。