保证一周更两篇吧,以此来督促自己好好的学习!代码的很多地方我都给予了详细的解释,帮助理解。好了,干就完了~加油!
声明:本python数据结构与算法是imooc上liuyubobobo老师java数据结构的python改写,并添加了一些自己的理解和新的东西,liuyubobobo老师真的是一位很棒的老师!超级喜欢他~
如有错误,还请小伙伴们不吝指出,一起学习~
一、什么是AVL树
- AVL树是最早的能够实现自平衡的二分搜索树!
- 传统的二分搜索树的局限性:如果以1,2,3,4,5,6的顺序添加元素,那么此时的二分搜索树将退化成一个链表,也就是说有很多操作变成O(n)的时间复杂度了。大大降低效率。而AVL树就能够避免这种情况。
- 什么是平衡二叉树?定义:对于任意一个节点,左子树和右子树的高度差不能超过1(这里要和完全二叉树的定义做个区分:完全二叉树的定义是任意叶子节点的高度差不超过1,所以平衡二叉树的定义要比完全二叉树的定义宽松了一些)。
例子:比如以前学过的线段树(是一棵满树,满树必为平衡二叉树)、堆(完全二叉树)
二、AVL树的节点和普通二分搜索树的节点的不同之处
- 需要树的每个节点存储额外的一个属性:高度(单一节点的高度为1,很明显,每个节点的高度为该节点左、右孩子节点它们二者中的最大高度+1)。
有了高度这个属性后,我们就能求二叉树的任意子树的平衡因子(我这里的定义为左树的高度减去右树的高度,当然也可以反过来,只是正负号的意义颠倒了)
三、图例
我所讲述的肯定不是最好的,AVL树的理解这方面还需要小伙伴们多多查阅相关资料!
1. 树高与平衡因子的作用
2. LL情形
- 上图中,我们可以看到:初始情况下:T1 < z < T2 < x < T3 < y < T4,右旋后:T1 < z < T2 < x < T3 < y < T4。所以仍然满足二分搜索树的性质。至于平衡性我在这里就不证明啦,涉及到一些假设,我怕表述不清楚误导大家。所以此时将原先的非平衡树转变成一棵平衡树了。
3. RR情形
- 初始:T1 < y < T2 < x < T3 < z < T4,左旋后:T1 < y < T2 < x < T3 < z < T4,没毛病~同理,平衡性我就不去证明了。
4. LR情形
- 初始:T2 < x < T3 < z < T4 < y < T1,先左旋,再右旋后:T2 < x < T3 < z < T4 < y < T1,也没毛病。
- 注意:上图中我把搁置节点的那一环给省略了,相信小伙伴们也能秒懂……这种情况下要先对非平衡节点的左孩子x进行左旋操作,此时让z成为非平衡节点的左孩子,然后再对非平衡节点y进行一次右旋操作即可。
5. RL情形
- 初始:T1 < y < T2 < z < T3 < x < T4,先右旋,在左旋后:T1 < y < T2 < z < T3 < x < T4,没毛病!
- 注意:上图中我把搁置节点的那一环给省略了,相信小伙伴们也能秒懂……这种情况下要先对非平衡节点的右孩子x进行右旋操作,此时让z成为非平衡节点的右孩子,然后再对非平衡节点y进行一次左旋操作即可。
四、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
测试完毕,无任何错误!
------------------------------
七、总结
- 本节把AVL树的增、删操作都实现了,剩下的不涉及增加或删除AVL树节点的操作都和原先的二分搜索树是一样的,这里也就不再实现了,有想了解其他操作的小伙伴可以去看一下我写的那篇关于二分搜索树的博客,如果二分搜索树都掌握不好的话,也就基本告别AVL树了O(∩_∩)O。。
- 基本上AVL树的相关操作的时间复杂度都是O(logn)的,绝对不会退化成O(n)的!
- AVL树的性能照红黑树还差那么点意思。。。但是这就好比快排会比归并排序快一点点一样,两者算法都是O(nlogn)的,红黑树的旋转操作对于avl树来说还是少一些。但是avl树可是历史上第一个实现自平衡的二叉搜索树啊!!还是很牛逼的!
- 下一小节做一个AVL树和常规的二分搜索树的性能对比吧,看一下AVL树的牛逼之处~
若有还可以改进、优化的地方,还请小伙伴们批评指正!