我们已经知道两种不同的方法来获取集合中的键值对. 回想一下这些集合如何实现ADT(抽象数据类型)MAP. 我们讨论了ADT MAP的两种实现,即基于列表的二进制搜索和哈希表. 在本节中,我们将学习二进制搜索树,它是另一个Map集合,其键指向值. 在这种情况下,我们无需考虑元素在树中的实际位置,但必须知道使用二叉树进行搜索更加有效.
搜索树操作
在研究此实现之前,让我们回顾一下ADT MAP提供的接口. 我们将注意到该接口与Python字典非常相似.
Map()创建一个新的空Map集合. put(key,val)向地图添加一个新的键值对. 如果键已在映射中,则旧值将替换为新值. get(key)提供一个键来返回保存在Map中的数据,或者返回None. del使用del map [key]语句从Map中删除键/值对. len()返回保存在Map中的键/值对的数量. 如果给定键在Map中,则使用map中的key语句返回True.
搜索树实现
对于二叉搜索树,如果左侧子树中的关键字小于父节点,而右侧子树中的关键字大于父节点的属性,我们将此树称为BST搜索树. 如前所述,当我们实现Map时,BST方法将指导我们实现这一目标. 图1显示了二进制搜索树的此功能. 显示的键没有任何值. 请注意,此属性适用于每个父节点和子节点. 左子树中的所有键都小于根节点,所有右子树中的键都大于根节点.
图1: 一个简单的二进制搜索树
现在您知道什么是二叉搜索树,让我们看一下如何构建二叉搜索树. 我们按照图1所示的顺序将这些键值插入搜索树中. 图1中的搜索树中的节点为70、31、93、94、14、23、73. 因为70是插入树中的第一个值,所以它是根节点. 接下来,31小于70,因此它是70的左子树. 接下来,93大于70,因此它是70的右子树. 我们现在填充了树的两个级别,因此下一个键值将是31或93的左或右子树. 由于94大于70和93,它成为93的右子树. 类似地,14小于70和31,因此它成为31的左子树. 23是也小于31,因此它必须是31的左子树. 但是,它大于14二叉排序树的实现,因此它是14的右子树.
为了实现二叉搜索树,我们将使用节点和引用的方法,这与我们实现链表和表达式树的过程类似. 因为我们必须能够创建和使用一个空的二进制搜索树,所以我们将使用两个类来实现它. 第一类称为BinarySearchTree,第二类称为TreeNode. BinarySearchTree类将对TreeNode类的引用作为二进制搜索树的根. 在大多数情况下,由外部类定义的外部方法仅需要检查树是否为空. 如果树上有节点,则需要BinarySearchTree类包含私有方法. 将根定义为参数. 在这种情况下,如果树为空,或者我们要删除树的根,则必须执行特殊操作. 清单1显示了BinarySearchTree类的构造函数和其他一些函数的代码.
列出1
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类的方法更易于实现. 如清单2所示,树节点的结构是通过这些辅助功能实现的. 如您所见,这些辅助函数可以根据节点的位置将其划分为左或右子节点类型以及子节点. TreeNode类非常清楚地跟踪每个父节点的属性. 当我们讨论删除操作的实现时,您将理解为什么这很重要.
清单2中有关TreeNode实现的另一个有趣之处是我们使用Python的可选参数. 可选参数使我们很容易在几种不同情况下创建树节点. 有时,即使我们已经有一个父节点和一个子节点,我们也想创建一个新的树节点. 与现有的父节点和子节点一样,我们可以使用父节点和子节点作为参数. 有时我们还会创建一个包含键值对的树,而不会传递父节点或子节点的任何参数. 在这种情况下,我们将使用可选参数的默认值.
清单2
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
现在我们有了BinarySearchTree和TreeNode类,现在该是编写put方法以使我们能够构建二进制搜索树的时候了. put方法是BinarySearchTree类的方法. 此方法将检查树是否具有根. 如果没有,我们将创建一个新的树节点并将其设置为树的根. 如果已经有根节点,我们将递归调用自身,使用辅助函数_put根据以下算法搜索树:
从树的根节点开始,搜索二叉树以将新键值与当前节点的键值进行比较. 如果新键值小于当前节点,请搜索左子树. 如果新键大于当前节点二叉排序树的实现,则搜索右子树.
当无法搜索左(或右)子树时,我们在树中的位置就是设置新节点的位置.
在树中添加一个节点,创建一个新的TreeNode对象,然后将此对象插入到先前的节点中.
清单3显示了在树中插入新节点的Python代码. _put函数需要根据上述步骤编写一个递归算法. 请注意,插入新的子树时,当前节点(CurrentNode)作为父节点传递到新树.
我们执行插入操作的一个重要问题是不能正确处理重复的键值. 我们的树实现了键值复制. 它将在右子树中创建一个与原始节点具有相同键值的新节点. 这样的结果是在搜索过程中将不会发现新节点. 我们使用一种更好的方式来处理插入重复的键值,将旧值替换为与新键关联的值. 我们会将这个错误修复作为练习留给您.
清单3
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)
通过put方法的实现,我们可以轻松地通过__setitem__方法重载[]作为运算符来调用put方法(请参见清单4). 这使我们可以编写类似于myZipTree ['Plymouth'] = 55446的python语句,看起来像Python字典.
清单4
def __setitem__(self,k,v):
self.put(k,v)
图2说明了将新节点插入二叉搜索树的过程. 灰色节点显示插入期间遍历树节点的顺序.
图2: 插入键值为= 19的节点
构造树后,下一个任务是检索给定的键. get方法比put方法更容易,因为它简单地递归搜索树,直到找到不匹配的叶节点或找到匹配的键值为止. 找到匹配的键值后,将返回节点中的值.
清单5显示了get,_get和__getitem__的代码. _get方法搜索的代码具有与put方法相同的选择左或右子树的逻辑. 请注意,_get方法返回TreeNode中的get值. _get可以用作灵活有效的方法,为可能需要使用TreeNode中数据的BinarySearchTree其他方法提供参数.
通过实现__getitem__方法,我们可以编写看起来像在访问字典的Python语句,实际上,我们只操作了二进制搜索树,例如Z = myziptree ['fargo']. 如您所见,__getitem__方法正在调用get.
清单5
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)
使用get,我们可以通过编写BinarySearchTree __contains__方法来实现该操作. __contains__方法只是调用get方法,如果有返回值,则返回True,如果没有则返回False. 如清单6所示.
清单6
def __contains__(self,key):
if self._get(key,self.root):
return True
else:
return False
回想起__contains__重载的运算符,它使我们可以编写如下语句:
本文来自电脑杂谈,转载请注明本文网址:
http://www.pc-fly.com/a/jisuanjixue/article-252377-1.html