**二叉搜索树(BST binary search tree)**是一种比较特殊的二叉树,表现为任意节点的值都比左孩子的值要大,而且小于等于右孩子的值,采用中序遍历BST(Binary Search Tree)就可以的到排序好的元素集合,而且插入删除的时间消耗也比较合理,但是有一个缺点就是内存开销有点大。
二叉搜索树的性质
1,任意节点x,其左子树中的key不大于x.key,其右子树中的key不小于x.key。
2,不同的二叉搜索树可以代表同一组值的集合。
3,二叉搜索树的基本操作和树的高度成正比,所以如果是一棵完全二叉树的话最坏运行时间为Θ(lgn),但是若是一个n个节点连接成的线性树,那么最坏运行时间是Θ(n)。
4,根节点是唯一一个parent指针指向NIL节点的节点。
5,每一个节点至少包括key、left、right与parent四个属性,构建二叉搜索树时,必须存在针对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
class BinarySearchTree:
def __init__(self):
self.root = None
self.size = 0
现在我们有了 BinarySearchTree shell 和 TreeNode,现在是时候编写 put 方法,这将允许我们构建二叉搜索树。 put 方法是BinarySearchTree 类的一个方法。此方法将检查树是否已具有根。如果没有根,那么 put 将创建一个新的 TreeNode 并将其做为树的根。如果根节点已经就位,则 put 调用私有递归辅助函数 _put 根据以下算法搜索树:
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)
上面展示了在树中插入一个新节点的 Python 代码。_put 函数按照上述步骤递归编写。请注意,当一个新的子节点插入到树中时,currentNode 将作为父节点传递给新的树节点。
我们实现插入的一个重要问题是重复的键不能正确处理。当我们的树被实现时,重复键将在具有原始键的节点的右子树中创建具有相同键值的新节点。这样做的结果是,具有新键的节点将永远不会在搜索期间被找到。处理插入重复键的更好方法是将新键相关联的值替换旧值。
一旦树被构造,下一个任务是实现对给定键的值的检索。get 方法比 put 方法更容易,因为它只是递归地搜索树,直到它到达不匹配的叶节点或找到匹配的键。当找到匹配的键时,返回存储在节点的有效载荷中的值。
当二叉查找树不为空时:
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 方法搜索以找到需要删除的 TreeNode。 如果树只有一个节点,这意味着我们删除树的根,但是我们仍然必须检查以确保根的键匹配要删除的键。 在任一情况下,如果未找到键,del 操作符将引发错误。
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 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
二叉搜索树的深度越小,那么搜索所需要的运算时间越小。一个深度为log(n)的二叉搜索树,搜索算法的时间复杂度也是log(n)。然而,我们在二叉搜索树中已经实现的插入和删除操作并不能让保持log(n)的深度。如果我们按照8,7,6,5,4,3,2,1的顺序插入节点,那么就是一个深度为n的二叉树。那么,搜索算法的时间复杂度为n。
在上一节中我们讨论了建立一个二叉搜索树。我们知道,当树变得不平衡时get和put操作会使二叉搜索树的性能降低到O(n)。
如何减少树的深度呢?
一种想法是先填满一层,再去填充下一层,这样就是一个完全二叉树(complete binary tree)。这样的二叉树实现插入算法会比较复杂。另一种比较容易实现的树状数据结构——AVL树,其搜索算法复杂度为log(n)。
在这一节中我们将看到一种特殊的二叉搜索树,它可以自动进行调整,以确保树随时都保持平衡。这种树被称为AVL树,命名源于其发明者:G.M. Adelson-Velskii 和 E.M. Landis。
AVL树实现 Map 抽象数据类型就像一个常规的二叉搜索树,唯一的区别是树的执行方式。为了实现我们的 AVL树,我们需要跟踪树中每个节点的平衡因子。我们通过查看每个节点的左右子树的高度来做到这一点。更正式地,我们将节点的平衡因子定义为左子树的高度和右子树的高度之间的差。
b a l a n c e F a c t o r = h e i g h t ( l e f t S u b T r e e ) − h e i g h t ( r i g h t S u b T r e e ) balanceFactor = height(leftSubTree) - height(rightSubTree) balanceFactor=height(leftSubTree)−height(rightSubTree)
使用上面给出的平衡因子的定义,我们说如果平衡因子大于零,则子树是左重的。如果平衡因子小于零,则子树是右重的。如果平衡因子是零,那么树是完美的平衡。为了实现AVL树,并且获得具有平衡树的好处,如果平衡因子是 -1,0 或 1,我们将定义树平衡。一旦树中的节点的平衡因子是在这个范围之外,我们将需要一个程序来使树恢复平衡。
高度为h的树的节点数(Nh)为:
Nh=1+Nh−1+Nh−2这个公式和斐波那契序列非常相似。我们可以利用这个公式通过树中的节点的数目推导出一个AVL树的高度。随着斐波那契序列的数字越来越大,Fi / Fi−1 越来越接近于黄金比例 Φ
在任何时候,我们的AVL树的高度等于树中节点数目的对数的常数(1.44)倍。 这是搜索我们的AVL树的好消息,因为它将搜索限制为 O(logN)。
当新增一个节点时,会破坏二叉树的平衡,因此需要通过旋转来修正。
注:由于新根是旧根的左节点,移动后的旧根的左节点一定为空。这时可以直接给移动后的旧根添加左节点。
同理,执行左旋转我们需要做到以下几点:
如图,如果子节点是4不是2,尝试单旋转,会发现无法解决问题。
要解决这个问题,我们必须使用以下规则:
此时解决方式为双旋转。
双旋转实际上是进行两次单旋转: 4为根节点的子树先进行一次向左的单旋转,然后将5为根节点的子树进行了一次向右的单旋转。这样恢复了树的ACL性质。
对于AVL树,可以证明,在新增一个节点时,总可以通过一次旋转恢复AVL树的性质。
当我们插入一个新的节点时,在哪里旋转?是用单旋转还是双旋转?
我们按照如下基本步骤进行:
红黑树是一种常用的平衡二叉搜索树,它是复杂的,但它的操作有着良好的最坏情况运行时间,即操作的复杂度都不会恶化,并且在实践中是高效的: 它可以在O(logn)时间内做单次的查找,插入和删除,这里的n是树中元素的数目。
红黑树是一种很有意思的平衡检索树。它的统计性能要好于平衡二叉树( AVL-树),因此,红黑树在很多地方都有应用。在C++ STL中,很多部分(目前包括set, multiset, map, multimap)应用了红黑树的变体(SGI STL中的红黑树有一些变化,这些修改提供了更好的性能,以及对set操作的支持)。
红黑树的主要规则:
红-黑树的主要规则如下:
在红-黑树中插入的节点都是红色的,这不是偶然的,因为插入一个红色节点比插入一个黑色节点违背红-黑规则的可能性更小。原因是:插入黑色节点总会改变黑色高度(违背规则4),但是插入红色节点只有一半的机会会违背规则3。另外违背规则3比违背规则4要更容易修正。
当插入一个新的节点时,可能会破坏这种平衡性,红-黑树主要通过三种方式对平衡进行修正,改变节点颜色、左旋和右旋。(具体规则省略)
因为每一个红黑树也是一个特化的二叉查找树,因此红黑树上的只读操作与普通二叉查找树上的只读操作相同。然而,在红黑树上进行插入操作和删除操作会导致不再符合红黑树的性质。恢复红黑树的性质需要少量(O(logn))的颜色变更(实际是非常快速的)和不超过三次树旋转(对于插入操作是两次)。虽然插入和删除很复杂,但操作时间仍可以保持为O(logn) 次。
一棵有 n 个内部节点的红黑树的高度至多为 2lg(n+1)。
证明略,参考最后一个链接。
参考:
https://facert.gitbooks.io/python-data-structure-cn/6.树和树的算法/6.13.查找树实现/
python实现二叉查找树(代码简单)
纸上谈兵: AVL树
[Data Structure] 数据结构中各种树
红黑树相关定理及其证明