摘要:现在对我们来说首要的问题是从二叉搜索树中删除一个节点。搜索树分析通过二叉搜索树实现的完成,我们将对我们已经实现的方法进行一个快速的分析。对它的执行效果造成限制的是二叉树的高度。当表示树的高度时,一个满二叉树中节点的总数是。
搜索树实现(续)
最后,我们把注意力转向二叉搜索树中最具挑战性的方法,删除一个键值(参见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也许看上去更复杂,因为它也许需要在删除操作完成之前一直查找后继节点。但是记得,寻找后继节点的最坏情形也是取决于树的高度,这意味着只需要简单地把工作量加倍,因为加倍是乘以一个常数因子,所以它不会改变最坏情形的复杂度。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/37756.html
摘要:图展示了二叉搜索树的这一特性,显示的键没有关联任何的值。因为我们必须能够创建和使用一个空的二叉搜索树,所以我们将使用两个类来实现,第一个类我们称之为,第二个类我们称之为。图说明了将新节点插入到一个二叉搜索树的过程。 二叉搜索树 我们已经知道了在一个集合中获取键值对的两种不同的方法。回忆一下这些集合是如何实现ADT(抽象数据类型)MAP的。我们讨论两种ADT MAP的实现方式,基于列表的...
摘要:我们知道,当树变得不平衡时和操作会使二叉搜索树的性能降低到。树实现抽象数据类型就像一个普通的二叉搜索树,唯一不同的是这棵树的工作方式。我们通过比较每个节点的左右子树的高度完成比较。 平衡二叉搜索树 在上一节中我们讨论了建立一个二叉搜索树。我们知道,当树变得不平衡时get和put操作会使二叉搜索树的性能降低到O(n)。在这一节中我们将看到一种特殊的二叉搜索树,它可以自动进行调整,以确保树...
摘要:二叉搜索树不一定是完全二叉树,因此不能像堆一样可以数组表示。在当前为根节点的子树中,查找为的节点。否则递归地到左右子树中找到为的节点,并更新。当当前节点有左孩子时,获得左子树中最小的节点。以上是我所了解的二叉搜索树的基本功能 二叉查找树或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均...
阅读 1644·2023-04-26 00:30
阅读 3116·2021-11-25 09:43
阅读 2773·2021-11-22 14:56
阅读 3165·2021-11-04 16:15
阅读 1111·2021-09-07 09:58
阅读 1999·2019-08-29 13:14
阅读 3087·2019-08-29 12:55
阅读 963·2019-08-29 10:57