保证一周更两篇吧,以此来督促自己好好的学习!代码的很多地方我都给予了详细的解释,帮助理解。好了,干就完了~加油!
声明:本python数据结构与算法是imooc上liuyubobobo老师java数据结构的python改写,并添加了一些自己的理解和新的东西,liuyubobobo老师真的是一位很棒的老师!超级喜欢他~
如有错误,还请小伙伴们不吝指出,一起学习~
我所讲述的肯定不是最好的,AVL树的理解这方面还需要小伙伴们多多查阅相关资料!
# -*- coding: utf-8 -*-
# Author: Annihilation7
# Data: 2019-02-16 05:07 pm
# Python version: 3.6
# 本小节中有很多代码和以前的二分搜索树的代码一样,就不做讲解了,有不明白的地方去二分搜索树那小节的代码看看吧^_^
class Node:
def __init__(self, value):
self.value = value # 这里实现的avl树相当于一个不包含重复元素的集合。
self.left = None
self.right = None # 前面的属性都和二分搜索树的属性保持一致
self.height = 1 # 新的属性:高度,默认值为1.
# 为什么默认值是1?因为二分搜索树每次在添加节点的时候,最终都会把这个节点放到一个合适的叶子节点的地方,所以此时
# 该新节点的高度一定为1,没毛病!
class Avltree:
def __init__(self):
self._root = None
self._size = 0
def __repr__(self):
return 'AVL树'
def isEmpty(self):
return self._size == 0
def getSize(self):
return self._size
# 两个用于检验的成员函数
# 由于avltree本质上还是一棵二分搜索树,所以必须要满足二分搜索树的性质。而且它还得是一棵平衡二叉树,
# 因此设计两个成员函数来检验当前树的这两个性质,以保证所有的操作都不会打破这两个性质
def isBst(self):
"""
检验当前的avltree是否是一棵二分搜索树
Returns:
是返回True,不是返回False
"""
def _isBst(node):
"""
检验以node为根节点的二叉树是否是一棵二分搜索树,利用二分搜索树的中序遍历的性质进行验证,因为中序遍历的输出结果是有序的!
Params:
- node: 传入的以node为根的二分搜索树
Returns:
是返回True,不是返回False
"""
def inOrder(node, alist):
"""
对以node为根节点的二分搜索树进行中序遍历
Params:
- node: 传入的以node为根的二分搜索树
- alist: 保存中序遍历结果的数组
"""
if node is None:
return
inOrder(node.left, alist) # 先左子树
alist.append(node.value) # 再自己,此时不是打印了,而是将当前节点的value添加进alist中
inOrder(node.right, alist) # 再右子树
record_list = []
inOrder(node, record_list) # 中序遍历的同时更新record_list
for i in range(1, len(record_list)): # 检验,有一个相邻的元素不满足严格升序就返回False
if record_list[i] < record_list[i - 1]:
return False
return True # 最后返回True
return _isBst(self._root) # 调用_isBst子函数,其实就是我想把self._root给封装起来,这样self._root不暴露给用户,比较舒服。。
def isBalanced(self):
"""
判断当前的avltree是否是一棵平衡二叉树
Returns:
是返回True,不是返回False
"""
def _isBalanced(node):
"""
判断以node为根节点的二叉树是否是一棵平衡二叉树
Params:
- node: 输入的根节点
Returns:
是返回True,不是返回False
"""
if node is None:
return True # 空树是平衡的,所以返回True。也是递归到底的情况。
if abs(self._getBalanceFactor(node)) > 1:
return False # 当前node的平衡因子的绝对值大于1了,已经不平衡了,所以直接返回False
return _isBalanced(node.left) and _isBalanced(node.right) # 否则就递归的去看node的左子树和右子树是否都满足平衡二叉树的性质,都满足才可以
# 所以用 与 操作。
return _isBalanced(self._root) # 直接调用_isBalanced函数,同理,我只是想把self._root封装一下。
# avl树的两个灵魂操作
# 对节点y进行右旋转操作,返回旋转后新的根节点x
# y x
# / \ / \
# x T4 向右旋转(y) z y
# / \ -----------> / \ / \
# z T3 T1 T2 T3 T4
# / \
# T1 T2
def rightRotate(self, y):
"""
将以不平衡节点y为根的二分搜索树进行右旋转操作
Params:
- node: 传入的不平衡节点
Returns:
旋转后的树的新根(即x)
"""
x = y.left # 记录x 和 T3
T3 = x.right
x.right = y # 进行右旋操作
y.left = T3
# 高度更新操作,T1, T2, T3, T4以及z还是保持原先的相对位置不变,所以它们的height不用更新
# 而x和y的height发生了改变,所以只需更新它俩。
# 并且要先更新y的height,再更新x的height。因为旋转后x的height是和y.height有关的!
y.height = max(self._getHeight(y.left), self._getHeight(y.right)) + 1
x.height = max(self._getHeight(x.left), self._getHeight(x.right)) + 1
return x # 返回新的根节点x
# 对节点y进行右旋转操作,返回旋转后的新的根节点x
# y x
# / \ / \
# T1 x 向左旋转(y) y z
# / \ -----------> / \ / \
# T2 z T1 T2 T3 T4
# / \
# T3 T4
def leftRotate(self, y):
"""
将以不平衡节点y为根的二分搜索树进行左旋转操作
Params:
- node: 传入的不平衡节点
Returns:
旋转后的树的新根(即x)
"""
x = y.right # 记录x以及x的左子树的根节点T2
T2 = x.left
x.left = y # 左旋转操作
y.right = T2
y.height = max(self._getHeight(y.left), self._getHeight(y.right)) + 1 # 节点高度更新操作
x.height = max(self._getHeight(x.left), self._getHeight(x.right)) + 1
return x # 将新的根节点返回
def contains(self, value):
"""
判断value是否存在于avl树中(设计这个函数其实没什么意义,和二分搜索树是完全一样的,但是性能对比会用到,就写在这了。)
Params:
- value: 待查询的值
Returns:
存在为True,否则为False
"""
def _contains(node, value):
"""
查询以node为根节点的二叉树是否包含value
Params:
- node: 传入的根节点
- value: 待查询的值
Returns:
存在为True,否则为False
"""
if node is None:
return False
if value < node.value:
return _contains(node.left, value)
elif node.value < value:
return _contains(node.right, value)
else:
return True
return _contains(self._root, value)
def add(self, value):
"""
删除值为value的节点
Params:
- value: 待添加的值
"""
self._root = self._add(self._root, value) # 调用私有函数_add
def remove(self, value):
"""
删除值为value的节点
Params:
- value: 待删除的值
"""
self._root = self._remove(self._root, value) # 调用私有函数_remove
# private
def _getHeight(self, node):
"""
求得一个节点的高度
Params:
- node: 传入的以node为根节点的二叉树
Returns:
该节点的高度
"""
if node is None:
return 0 # node本身为None的情况,如单一节点的左右孩子都为None,此时返回0
return node.height # 否则返回该node的height属性
def _getBalanceFactor(self, node):
"""
求某个节点的平衡因子
Params:
- node: 传入的节点
Returns:
该节点的平衡因子值(注意我是用左子树的高度减去右子树的高度,你也可以反过来,只是有的地方的逻辑需要变更)
"""
if node is None:
return 0 # 如果node本身为None,返回0.即认空树是平衡的,很合理。
return self._getHeight(node.left) - self._getHeight(node.right) # 左孩子的树高减去右孩子的树高
def _add(self, node, value):
"""
将值value添加进以node为根的二分搜索树中
Params:
node: 二分搜索树的根
value: 待添加的值
Returns:
添加节点后的新的根节点
"""
if node is None:
self._size += 1
return Node(value)
if node.value < value:
node.right = self._add(node.right, value)
elif value < node.value:
node.left = self._add(node.left, value)
# 如果value本身就存在于二分搜索树中的话我们什么也不做,相当于该树不包含重复的元素
# 添加完元素后在回归的过程中,涉及到添加新节点的这条路径上的节点的高度会发生变化
# 所以我们这里要对它们的height属性进行更新,很简单
node.height = max(self._getHeight(node.left), self._getHeight(node.right)) + 1
# 前面讲过了,就是左右孩子节点的最大高度 + 1
node_balance = self._getBalanceFactor(node) # 求得当前节点的平衡银子
# 维护平衡操作
# (LL)
if node_balance > 1 and self._getBalanceFactor(node.left) >= 0: # 此时保证一定是LL情形
# 此时插入的节点在不平衡节点的左侧的左侧(LL),一次右旋操作即可
return self.rightRotate(node) # 直接返回右旋后的根节点作为原先父亲节点的孩子
# (RR)
if node_balance < -1 and self._getBalanceFactor(node.right) <= 0: # 保证一定是RR情形
# 此时插入的节点在不平衡节点的右侧的右侧(RR),一次左旋操作即可
return self.leftRotate(node) # 返回左旋后的根节点作为原先父亲节点的孩子
# (LR)
if node_balance > 1 and self._getBalanceFactor(node.left) < 0:
# 对于node来说是左子树高度比右子树高度高至少2,但是对于node的右孩子来说,其右子树高度比左子树高度高至少1,保证是LR情形
# 此时先对非平衡节点的左孩子进行左旋操作,将得到的新的根节点赋给非平衡节点的左孩子
node.left = self.leftRotate(node.left)
return self.rightRotate(node) # 最后对非平衡节点进行一次右旋操作,并将得到的新的根节点返回
# (RL)
if node_balance < -1 and self._getBalanceFactor(node.right) > 0:
# 对于node来说是右子树高度比左子树高度高至少2,但是对于node的左孩子来说,其左子树高度比右子树高度高至少1,保证是LR情形
# 此时先对非平衡节点的右孩子进行右旋操作,将得到的新的根节点赋给非平衡节点的右孩子
node.right = self.rightRotate(node.right)
return self.leftRotate(node) # 最后对非平衡节点进行一次左旋操作,并将得到的新的根节点返回
return node # 不需要旋转的话就把当前节点return,最终回溯到新的根节点。
def _remove(self, node, value):
"""
删除以node为根节点的avl树中值为value的节点
Params:
- node: 树的根节点
- value: 待删除的值
Returns:
删除节点后的新的根节点
"""
def minimum(node):
"""
查找以node为根的二分搜索树的携带最小值的节点,并将这个节点返回
Params:
- node: 输入的二分搜索树的根节点
Returns:
在以node为根的二分搜索树中的携带最小值的节点
"""
if node.left is None:
return node
return minimum(node.left)
if node is None:
return None # 递归到底
# 在删除节点后的回溯过程中,有可能打破avl树的平衡性,所以之前是逐层返回的方法已经会有bug了,这里我们
# 不再直接return,而是用retNode进行标记,然后看一下retNode是否需要进行旋转。
retNode = None
if value < node.value:
node.left = self._remove(node.left, value)
retNode = node
elif node.value < value:
node.right = self._remove(node.right, value)
retNode = node
else:
if node.left is None:
tmp_node = node.right
node.right = None # 便于回收期回收
self._size -= 1
retNode = tmp_node
elif node.right is None:
tmp_node = node.left
node.left = None
self._size -= 1
retNode = tmp_node
else:
# 我这里选择后继节点,当然你也可以选择前驱节点
# 如果有疑惑,还请移步二分搜索树那一节^_^
successor_node = minimum(node.right) # 找到后继节点
# 这里可以写一个remove_minimum函数,但是注意在remove_minimum的过程中也会打破平衡性,所以也要进行旋转操作
# 为防止代码冗余,这里我就不写remove_minimum函数了,因为此时我们已经拿到了successor_node,删除的值为successor_node
# 节点所携带的值!!
successor_node.right = self._remove(node.right, successor_node.value) # 也就是又进行了一次递归删除
# node右子树的携带最小值的节点,并返回删除节点后的根节点,所以一点问题
# 都没有,不清楚了一定要去二分搜索树那一节好好的消化一下!
self._size += 1 # 不是要删除右子树携带最小值的节点,而是要让他去取代当前的node,真正要删除的节点是当前的node。所以前面减的1要加回来。
successor_node.left = node.left # 便于垃圾回收
node.left = node.right = None
self._size -= 1
retNode = successor_node
if retNode is None:
# 这里的一个小bug的修复~非常的难找,就是如果删除的是一个单节点的二分搜索树(即叶节点),
# 而要删除的值和这个叶节点所携带的值并不想等,那么返回的就是None呀,此时后面的逻辑就会出错了,
# 所以要在这里把这个小bug修复掉
return None
# 删除元素中的平衡的逻辑和add的平衡逻辑完全一致!!!
# 也是在回溯的过程中维护avl树的平衡性。我这里直接粘贴过来了哈。。
# 删除完元素后在回归的过程中,涉及到删除新节点的这条路径上的节点的高度会发生变化
# 所以我们这里要对它们的height属性进行更新,很简单
retNode.height = max(self._getHeight(retNode.left), self._getHeight(retNode.right)) + 1
# 前面讲过了,就是左右孩子节点的最大高度 + 1
node_balance = self._getBalanceFactor(retNode) # 求得当前节点的平衡银子
# 维护平衡操作
# (LL)
if node_balance > 1 and self._getBalanceFactor(retNode.left) >= 0: # 此时保证一定是LL情形
# 此时插入的节点在不平衡节点的左侧的左侧(LL),一次右旋操作即可
return self.rightRotate(retNode) # 直接返回右旋后的根节点作为原先父亲节点的孩子
# (RR)
if node_balance < -1 and self._getBalanceFactor(retNode.right) <= 0: # 此时保证一定是RR情形
# 此时插入的节点在不平衡节点的右侧的右侧(RR),一次左旋操作即可
return self.leftRotate(retNode) # 返回左旋后的根节点作为原先父亲节点的孩子
# (LR)
if node_balance > 1 and self._getBalanceFactor(retNode.left) < 0:
# 对于retNode来说是左子树高度比右子树高度高至少2,但是对于retNode的右孩子来说,其右子树高度比左子树高度高至少1,保证是LR情形
# 此时先对非平衡节点的左孩子进行左旋操作,将得到的新的根节点赋给非平衡节点的左孩子
retNode.left = self.leftRotate(retNode.left) # 这两步看图即懂
return self.rightRotate(retNode)
# (RL)
if node_balance < -1 and self._getBalanceFactor(retNode.right) > 0:
# 对于retNode来说是右子树高度比左子树高度高至少2,但是对于retNode的左孩子来说,其左子树高度比右子树高度高至少1,保证是LR情形
# 此时先对非平衡节点的右孩子进行右旋操作,将得到的新的根节点赋给非平衡节点的右孩子
retNode.right = self.rightRotate(retNode.right) # 这两步看图即懂
return self.leftRotate(retNode)
return retNode # 最后将retNode return回去,从而回溯到新的根节点。
# -*- coding: utf-8 -*-
# Author: Annihilation7
# Data: 2019-02-16 10:35 pm
# Python version: 3.6
import avl_tree
import numpy as np
def test():
test_avl = avl_tree.Avltree()
op_nums = 1000 # 1000次测试操作
epoches = 5 # 共随机测试5轮
for epoch in range(epoches):
print('第 {}/{} 轮测试:'.format(epoch + 1, 5))
add_list = [np.random.randint(10000) for i in range(op_nums)]
for elem in add_list:
test_avl.add(elem) # 500次添加操作
print('元素添加完毕,下面进行验证:')
print('size: {}'.format(test_avl.getSize()))
print('是否满足二分搜索树的性质:{}'.format(test_avl.isBst()))
print('是否满足平衡二叉树的性质:{}'.format(test_avl.isBalanced()))
print('下面测试avl树的删除节点的操作的正确性:')
for elem in add_list:
test_avl.remove(elem)
# 每次删除完一个元素我们都验证一下
if not test_avl.isBst():
raise Exception('在删除 {} 元素的时候,此时avl树不满足二分搜索树的性质!'.format(i))
if not test_avl.isBalanced():
raise Exception('在删除 {} 元素的时候,此时avl树不满足平衡二叉树的性质!'.format(i))
print('...')
print('删除完毕,此时的size: {}'.format(test_avl.getSize()))
print('测试完毕,无任何错误!')
print('-' * 30)
if __name__ == '__main__':
test()
第 1/5 轮测试:
元素添加完毕,下面进行验证:
size: 950
是否满足二分搜索树的性质:True
是否满足平衡二叉树的性质:True
下面测试avl树的删除节点的操作的正确性:
...
删除完毕,此时的size: 0
测试完毕,无任何错误!
------------------------------
第 2/5 轮测试:
元素添加完毕,下面进行验证:
size: 948
是否满足二分搜索树的性质:True
是否满足平衡二叉树的性质:True
下面测试avl树的删除节点的操作的正确性:
...
删除完毕,此时的size: 0
测试完毕,无任何错误!
------------------------------
第 3/5 轮测试:
元素添加完毕,下面进行验证:
size: 941
是否满足二分搜索树的性质:True
是否满足平衡二叉树的性质:True
下面测试avl树的删除节点的操作的正确性:
...
删除完毕,此时的size: 0
测试完毕,无任何错误!
------------------------------
第 4/5 轮测试:
元素添加完毕,下面进行验证:
size: 957
是否满足二分搜索树的性质:True
是否满足平衡二叉树的性质:True
下面测试avl树的删除节点的操作的正确性:
...
删除完毕,此时的size: 0
测试完毕,无任何错误!
------------------------------
第 5/5 轮测试:
元素添加完毕,下面进行验证:
size: 957
是否满足二分搜索树的性质:True
是否满足平衡二叉树的性质:True
下面测试avl树的删除节点的操作的正确性:
...
删除完毕,此时的size: 0
测试完毕,无任何错误!
------------------------------
若有还可以改进、优化的地方,还请小伙伴们批评指正!