定义一:树由节点及连接节点的边构成。
树有以下属性:
❏ 有一个根节点;
❏ 除根节点外,其他每个节点都与其唯一的父节点相连;
❏ 从根节点到其他每个节点都有且仅有一条路径;
❏ 如果每个节点最多有两个子节点,我们就称这样的树为二叉树。
定义二:(递归定义)
一棵树要么为空,要么由一个根节点和零棵或多棵子树构成,子树本身也是一棵树。每棵子树的根节点通过一条边连到父树的根节点。
实现
以下函数创建并操作二叉树
实现树的关键在于选择一个好的内部存储技巧。Python提供两种有意思的方式,我们在选择前会仔细了解这两种方式。
- 第一种称作“列表之列表”
- 第二种称作“节点与引用”。
列表之列表
“列表之列表”表示法有个很好的性质,那就是表示子树的列表结构符合树的定义,这样的结构是递归的!
还有一个很好的性质,那就是这种表示法可以推广到有很多子树的情况。如果树不是二叉树,则多一棵子树只是多一个列表。
def BinaryTree(r):
return [r,[],[]]
def insertLeft(root,newBracnch):
t=root.pop(1)
if len(t)>1:
#如果插入节点有左子树,就把其作为newBracnch的左子树
root.insert(1,[newBracnch,t,[]])
else:
root.insert(1,[newBracnch,[],[]])
return root
def rightLeft(root,newBracnch):
t=root.pop(2)
if len(t)>1:
#如果插入节点有左子树,就把其作为newBracnch的左子树
root.insert(2,[newBracnch,[],t])
else:
root.insert(2,[newBracnch,[],[]])
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)
print(r)
insertLeft(r,4)
print(r)
6.4.2 节点与引用
节点与引用”表示法的要点是,属性left和right会指向BinaryTree类的其他实例。举例来说,在向树中插入新的左子树时,我们会创建另一个BinaryTree实例,并将根节点的self.leftChild改为指向新树。
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.leftChild=self.leftChild
self.leftChild=t
def insertRight(self,newNode):
if self.rightChild==None:
self.rightChild=BinaryTree(newNode)
else:
t=BinaryTree(newNode)
t.rightChild=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")
r.insertLeft("b")
print(r.getLeftChild())
二叉树的应用
解析树
- ❏ 如何根据完全括号表达式构建解析树;
- ❏ 如何计算解析树中的表达式;
- ❏ 如何将解析树还原成最初的数学表达式。
((7 + 3) ∗ (5-2))
可以表示为:
构建解析树的第一步是将表达式字符串拆分成标记列表。
标记列表中有四种元素: ( , 运算符 , 运算数 , )
定义下列四个规则:
- 如果当前标记是(,就为当前节点添加一个左子节点,并下沉至该子节点;
- 如果当前标记在列表[’+’, ‘-’, ‘/’,’*’]中,就将当前节点的值设为当前标记对应的运算符;为当前节点添加一个右子节点,并下沉至该子节点;
- 如果当前标记是数字,就将当前节点的值设为这个数并返回至父节点;
- 如果当前标记是),就跳到当前节点的父节点。
按照该规则,下列表达式的树构建过程如下:
[’(’, ‘3’, ‘+’, ‘(’, ‘4’, ‘*’, ‘5’, ‘)’, ‘)’]
本例表明,在构建解析树的过程中,需要追踪当前节点及其父节点。
如何追踪父节点呢?一个简单的办法就是在遍历这棵树时使用栈记录父节点。每当要下沉至当前节点的子节点时,先将当前节点压到栈中。当要返回到当前节点的父节点时,就将父节点从栈中弹出来。
from pythonds.basic import Stack
from pythonds.trees import BinaryTree
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 in "+-*/)":
currentTree.setRootVal(eval(i))
currentTree.insertRight("")
pStack.pusk(currentTree)
currentTree=currentTree.getRightChild()
elif i not in "+-*/)":
currentTree.setRootVal(eval(i))
paretnt=pStack.pop()
currentTree=paretnt
elif i==")":
currentTree=paretnt.pop()
else:
raise ValueError("unkown operator"+i)
return eTree
计算二叉解析树的递归函数
def evaluate(parseTree):
opers={"+":operator.add,"-":operator.sub,"*":operator.mul,"/":operatoe.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()
6.5.2 树的遍历
前序遍历
在前序遍历中,先访问根节点,然后递归地前序遍历左子树,最后递归地前序遍历右子树
(根,左,右)
中序遍历
在中序遍历中,先递归地中序遍历左子树,然后访问根节点,最后递归地中序遍历右子树。
(左根右)
后序遍历
在后序遍历中,先递归地后序遍历左子树,然后递归地后序右遍历子树,最后访问根节点。
(左右根)
将前序遍历算法实现为外部函数
def preorder(tree):
if tree:
print(tree.getRootVal())
#访问根节点
preorder(tree.getLeftChild())
#访问左节点
preorder(tree.getRightChild())
#访问右节点
将前序遍历算法实现为BinaryTree类的方法
def preorder(self):
print(self.key)
if self.leftChild:
self.leftChild.preorder()
if self.rightChild:
self.rightChild.preorder()
哪种实现方式更好呢?在本例中,将preorder实现为外部函数可能是更好的选择。
原因在于,很少会仅执行遍历操作,在大多数情况下,还要通过基本的遍历模式实现别的目标。
后序遍历函数
def postorder(tree):
if tree!=None:
preorder(tree.getLeftChild())
#访问左节点
preorder(tree.getRightChild())
#访问右节点
print(tree.getRootVal())
#访问根节点
我们已经见识过后序遍历的一个常见用途,那就是计算解析树。
def postordereval(tree):
opers={"+":operator.add,"-":operator.sub,"*":operator.mul,"/":operatoe.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 postorder(tree):
if tree!=None:
preorder(tree.getLeftChild())
#访问左节点
preorder(tree.getRightChild())
#访问右节点
print(tree.getRootVal())
#访问根节点
6.6 利用二叉堆实现优先级队列
队列有一个重要的变体,叫作优先级队列。
对于一些图算法来说,优先级队列是一个有用的数据结构。
现优先级队列的经典方法是使用叫作二叉堆的数据结构。
二叉堆的入队操作和出队操作均可达到O(log n)。
二叉堆有两个常见的变体:
最小堆(最小的元素一直在队首)与最大堆(最大的元素一直在队首)
为了使二叉堆能高效地工作,我们利用树的对数性质来表示它。
你会在6.7.3节学到,
为了保证对数性能,必须维持树的平衡。
平衡的二叉树是指,其根节点的左右子树含有数量大致相等的节点。
在实现二叉堆时,我们通过创建一棵完全二叉树来维持树的平衡。
在完全二叉树中,除了最底层,其他每一层的节点都是满的。
完全二叉树的另一个有趣之处在于,可以用一个列表来表示它,
而不需要采用“列表之列表”或“节点与引用”表示法。
中处于位置p的节点来说,它的左子节点正好处于位置2p;
同理,右子节点处于位置2p+1。
若要找到树中任意节点的父节点,
只需使用Python的整数除法即可。给定列表中位置n处的节点,其父节点的位置就是n/2。
堆的有序性是指:对于堆中任意元素x及其父元素p, p都不大于x
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]:
temp=self.heapList[i//2]
self.heapList[i//2]=self.heapList[i]
self.heapList[i]=tmp
i=i//2
def insert(self,k):
self.headList.appned(k)
self.currentSize=self.currentSize+1
self.percUp(self.currentSize)
思考题:
找到两个列表中
第k个小的数字?
找到一个列表中
第k个小的数字?
两个列表和一个列表解决问题的关键差异点是哪里呢?
编写delMin方法。
移除根节点之后需要做的操作:
第一步,取出列表中的最后一个元素,将其移到根节点的位置。移动最后一个元素保证了堆的结构性质,但可能会破坏二叉堆的有序性。第二步,将新的根节点沿着树推到正确的位置,以重获堆的有序性
`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-=1
self.heapList.pop()
self.perDown(1)
return retval
根据元素列表构建堆
def buildHeap(self,alist):
i=len(alist)//2
self.currentSize=len(alist)
self.heapList=[0]+alist[:]
while (i>0):
self.perDown(i)
i=i-1
利用建堆的时间复杂度为O(n)这一点,可以构造一个使用堆为列表排序的算法,使它的时间复杂度为O(n log n)。这个算法留作练习
堆排序算法
6.7 二叉搜索树
回想一下,我们讨论过映射抽象数据类型的两种实现,
它们分别是列表二分搜索和散列表。
本节将探讨二叉搜索树,它是映射的另一种实现。
6.7.1 搜索树的操作
复习一下映射抽象数据类型提供的接口。
你会发现,这个接口类似于Python字典。
6.7.2 搜索树的实现
二叉搜索树性质::小于父节点的键都在左子树中,大于父节点的键则都在右子树中。
6.7 二叉搜索树
我们已经学习了两种从集合中获取键-值对的方法。回想一下,我们讨论过映射抽象数据类型的两种实现,它们分别是列表二分搜索和散列表。本节将探讨二叉搜索树,它是映射的另一种实现。我们感兴趣的不是元素在树中的确切位置,而是如何利用二叉树结构提供高效的搜索。
6.8 平衡二叉搜索树
BinarySearchTree类
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 hasrRightChild(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 isRootNode(self):
return not self.parent
def isLeaf(self):
return not (self.leftChild or self.rightChild)
def hasAnyChild(self):
return self.leftChild or self.rightChild
def hasBothChild0(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.hasrRightChild():
self.rightChild.parent=self
现在有了BinarySearchTree和TreeNode,是时候写一个帮我们构建二叉搜索树的put方法了
搜索放置位置的方法:
❏ 从根节点开始搜索二叉树,比较新键与当前节点的键。如果新键更小,搜索左子树。如果新键更大,搜索右子树。
❏ 当没有可供搜索的左(右)子节点时,就说明找到了新键的插入位置。
❏ 向树中插入一个节点,做法是创建一个TreeNode对象,并将其插入到前一步发现的位置上。
为二叉搜索树插入新节点
def put(self,key,val):
if self.root:
self._put(key,val,self.root)
#存在根节点,调用_put函数放置合适位置
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,k,v):
self.put(k,v)
新节点的插入过程:
查找键对应的值
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("key not in tree")
elif self.size==1 and self.root.key==key:
self.root=None
self.size=self.size-1
else:
raise KeyError("key not in tree")
def __delitem__(self,key):
self.delete(key)
一旦找到待删除键对应的节点,就必须考虑3种情况。
删除的节点没有子节点
删除的节点只有一个子节点
删除的节点有两个子几点
remove函数实现比较复杂。暂时搁置好了。
pass
6.7.3 搜索树的分析
搁置。。
6.8 平衡二叉搜索树
6.8.3 映射实现总结
本章和第5章介绍了可以用来实现映射这一抽象数据类型的多种数据结构,包括有序列表、散列表、二叉搜索树以及AVL树。表6-1总结了每个数据结构的性能。