搜索树实现(续)
最后,我们把注意力转向二叉搜索树中最具挑战性的方法,删除一个键值(参见Listing 7)。首要任务是要找到搜索树中要删除的节点。如果树有一个以上的节点,我们使用_get方法找到需要删除的节点。如果树只有一个节点,这意味着我们要删除树的根,但是我们仍然要检查根的键值是否与要删除的键值匹配。在以上两种情况下,如果没有找到该键,del操作就会报错。
Listing 7
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)
一旦我们找到包含要删除的节点,我们必须考虑三种情况:
要删除的节点没有孩子(见图3).
要删除的节点只有一个孩子(见图4).
要删除的节点有两个孩子(见图5).
第一种情况是最简单的(参见Listing 8)。如果当前节点没有孩子,所有我们需要做的是引用删除该节点并删除父节点的引用。本例的代码显示在如下。
Listing 8
if currentNode.isLeaf():
if currentNode == currentNode.parent.leftChild:
currentNode.parent.leftChild = None
else:
currentNode.parent.rightChild = None
图 3:删除键值为16的节点,这个节点没有孩子
第二种情况只是稍微复杂(参见Listing 9)。如果节点只有一个孩子,那我们可以简单地让孩子替换它父母的位置。此案例的代码如下所示。看到这段代码,就会发现有六种情况要考虑。由于是具有左子树还是右子树的情况,我们只讨论当前节点只有左子树的情况。具体过程如下:
如果当前节点是左子树,那我们只需要更新左子树的引用指向当前节点的父节点,然后更新父节点的左子树引用指向当前节点的左子树。
如果当前节点是右子树,那我们只需要更新右子树的引用指向当前节点的父节点,然后更新父节点的右子树引用指向当前节点的右子树。
如果当前节点没有父节点,它一定是根。这种情况下,我们只需通过调用replaceNodeData方法把键替换为左子树和右子树里的数据。
Listing 9
else: # this node has one child
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)
图 4:删除键值为25的节点,它是只有一个孩子的节点
第三种情况是最难处理的情况(参见Listing 10)。如果一个节点有两个孩子,我们就不可能简单地让其中一个替换节点的位置,我们需要寻找一个节点,用来替换这个将要删除的节点,我们需要的这个节点能够保持现有二叉搜索树的左、右子树的关系。这个节点在树中具有第二大的键值。我们称这个节点为后继节点,我们将一路寻找这个后继节点,后继节点必须保证没有一个以上的孩子,所以既然我们已经知道如何处理这两种情况,我们就可以实现它了。一旦后继结点被删除,我们把它放在树中将被删除的树节点处。
图 5:删除键值为5的节点,它有两个孩子节点
第三种情况的处理代码如下所示。注意我们是用findSuccessor和findMin方法来辅助找到后继节点的。要删除后继节点,我们利用spliceOut方法。我们用spliceOut的原因是它能直接使我们想移除的节点,做出正确的变化。我们也可以调用递归删除,但那样我们就会浪费时间反复寻找关键节点。
Listing 10
elif currentNode.hasBothChildren(): #interior
succ = currentNode.findSuccessor()
succ.spliceOut()
currentNode.key = succ.key
currentNode.payload = succ.payload
找到后继节点的代码如下所示(参见Listing 11),你可以看到TreeNode类的一个方法。这个代码利用二叉搜索树中序遍历的性质,从最小到最大打印出树中的节点。当寻找后继节点时需要考虑三种情况:
如果节点有右子节点,那么后继节点是右子树中最小的关键节点。
如果节点没有右子节点,是其父节点的左子树,那么父节点是后继节点。
如果节点是其父节点的右子节点,而本身无右子节点,那么这个节点的后继节点是其父节点的后继节点,但不包括这个节点。
现在对我们来说首要的问题是从二叉搜索树中删除一个节点。
findMin方法是用来找到子树中的最小的节点。你要了解,最小值在任何二叉搜索树中都是树最左边的孩子节点。因此findMin方法只需要简单地追踪左子树,直到找到没有左子树的叶节点。
Listing 11
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
我们还需要看看二叉搜索树的最后一个接口。假设我们已经按顺序简单地遍历了子树上所有的键值,就肯定是用字典实现的,就会有疑问:为什么不是树?我们已经知道如何使用中序遍历二叉树的算法,然而,写一个迭代器需要更多的操作,因为每次调用迭代器时,一个迭代器只返回一个节点。
Python提供了一个创建迭代器的非常强大的功能。这个功能就是yield。yield类似于return,返回一个值给调用者。然而,yield也需要额外的步骤来暂停函数的执行,以便下次调用函数时继续执行时做准备。它的功能是创建可迭代的对象,称为生成器。
二叉树迭代器的代码如下所示。仔细观察这些代码:乍一看,你可能会认为代码是非递归的。但是请记住,__iter__重写了for x in的操作符进行迭代,所以它确实是递归!因为它是__iter__方法在TreeNode类中定义的TreeNode的实例递归。
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
在此,你可能想下载包含BinarySearchTree和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 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
class BinarySearchTree:
def __init__(self):
self.root = None
self.size = 0
def length(self):
return self.size
def __len__(self):
return self.size
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,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('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 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 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 remove(self,currentNode):
if currentNode.isLeaf(): #leaf
if currentNode == currentNode.parent.leftChild:
currentNode.parent.leftChild = None
else:
currentNode.parent.rightChild = None
elif currentNode.hasBothChildren(): #interior
succ = currentNode.findSuccessor()
succ.spliceOut()
currentNode.key = succ.key
currentNode.payload = succ.payload
else: # this node has one child
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)
mytree = BinarySearchTree()
mytree[3]="red"
mytree[4]="blue"
mytree[6]="yellow"
mytree[2]="at"
print(mytree[6])
print(mytree[2])
搜索树分析
通过二叉搜索树实现的完成,我们将对我们已经实现的方法进行一个快速的分析。让我们先看一下put这个方法。对它的执行效果造成限制的是二叉树的高度。回想一下语法阶段,树的高度指的是根和最深的叶节点之间的边的数目。高度作为一种限制因素是因为当我们在树中寻找一个合适的位置插入节点时,我们最多将会对树的每一级做比较。
二叉树的高度可能是多少呢?这个问题的答案取决于键是怎么被添加到树中的。如果键是以一个随机的顺序加到树中的,那么当节点的数目为n时,树的高度大概是 log2n。 这是因为键是随机地散布的,大约有一半的节点会比根节点大,另一半会比它小。记住在二叉树中,根上有一个节点,下一级有两个,再下一级有四个。当级数为d时,这一级的节点 数目为 2d。当h表示树的高度时,一个满二叉树中节点的总数是 2h+1-1。
一个满二叉树中的节点数目和一个平衡二叉树中的左子树和右子树的数目是一样多的。当树中有n个节点时put执行的最差结果的时间复杂度是 O(log2n)。要注意到这与上一段所说的是逆运算的关系。所以 log2n 代表树的高度,这表示把一个新节点插入到一个合适位置前,在搜索中需要比较的次数。
不巧的是,通过插入排序过后的键,建造一个高度为n的搜索树是可能的。图 6 就展示了这种树的一个例子,在这种情形下,这时put方法的时间复杂度为 O(n)。
图 6:一个倾斜的二叉搜索树性能会低下
现在你已经理解了put方法的效率会受到树的高度的限制,你可能猜测其他的方法,get,in和del也是受限制的,因为要在树上寻找键,最坏的情形就是一直搜索树最后却找不到键。乍一看del也许看上去更复杂,因为它也许需要在删除操作完成之前一直查找后继节点。但是记得,寻找后继节点的最坏情形也是取决于树的高度,这意味着只需要简单地把工作量加倍,因为加倍是乘以一个常数因子,所以它不会改变最坏情形的复杂度。