详细理解平衡二叉树AVL与Python实现
前言
上一篇文章讨论的二叉搜索树,其时间复杂度最好的情况下是O(log(n)),但是最坏的情况是O(n),什么时候是O(n)呢?
像这样:
如果先插入10,再插入20,再插入30,再插入40就会成上边这个样子
这个就像是双向链表,我们期望它是下面这个样子:
所以我们希望有一种策略能够将第一个图变成第二个图,或者说使树的结构不会产生像第一种图的形式
实现这种策略的一种方式是AVL树
AVL树
AVL树的名称是以它的发明家的名字命名的:Adel’son-Vel’skii和Landis
满足高度平衡属性的二叉树就是AVL树
高度平衡属性是:对于树中的每一个位置p,p的孩子的高度最多相差1
很显然前言中的第一个图并不满足高度平衡属性,第二个是满足的。
同时高度平衡属性也意味着一颗AVL树的子树同样是AVL树
并且可以通过证明(这里就不再证了)得到AVL树的高度是O(log n)
所以得出结论,AVL树可以使时间复杂度保持O(log n)
接下来的问题就是怎样保持二叉树的高度平衡属性
保持二叉树的高度平衡属性
要保持高度平衡属性的原因是破坏了高度平衡属性
破坏的方式有两种:添加节点与删除节点
添加节点如图:
添加50的时候,就会破坏高度平衡属性
删除节点如图:
删除10的时候也会破坏高度平衡属性
最后,不论是添加节点还是删除节点,都会使树变成非高度平衡的状态,这种非高度平衡的状态有4种:
1.LL
LL是left-left,可以理解为:首先它不平衡,其次根节点的左子树比右子树高,并且根节点的左子树的左子树比根节点的左子树的右子树高。(从上到下都是左边高)
2.LR
LR是left-right,可以理解为:首先它不平衡,其次根节点的左子树比右子树高,并且根节点的左子树的右子树比根节点的左子树的左子树高。(从上到下先左高后右高)
3.RR
RR是right-right,可以理解为:首先它不平衡,其次根节点的右子树比左子树高,并且根节点的右子树的右子树比根节点的右子树的左子树高。(从上到下都是右边高)
4.RL
RL是right-left,可以理解为:首先它不平衡,其次根节点的右子树比左子树高,并且根节点的右子树的左子树比根节点的右子树的右子树高。(从上到下先右高后左高)
最后,判断是哪种形式的非平衡状态,一定要从不平衡的节点位置看,并不是看4层,比如:
这里只有3层节点,不平衡的节点是20,20的左子树比右子树高,10的左子树比右子树高,所以是LL。(这里的高定义为节点5的高度为1,空节点的高度为0)
接下来是保持高度平衡的调整策略:
同样对于4种不同的形式有4种解决方案:
1.LL
这个变换就像是以10为中心,向右旋转,使10变成根节点,10的左子树不变,右子树变成了20,多余出的15正好挂在由于变换失去了左子树的20的左边。变换后结点从左到右的顺序依然没有变,所以15是正好挂在20的左边的。
2.RR
RR与LL形式差不多,只不顾是反着来的。相当于进行一次左旋转。
RR与LL都只进行一次旋转即可,而LR与RL需要进行两次旋转
3.LR
第一次相当于对5、10、15、17这棵子树进行了一次RR旋转,旋转方式与之前的RR方式相同,就像是以15为中心向左旋转,旋转的结果使得整棵树变成了LL的不平衡形态,然后再按照LL的旋转方式对整棵树处理。
4.RL
RL同样是LR的相反模式,先将22、25、30、40这棵子树进行LL旋转,再将整棵树进行RR旋转
理解了avl保持平衡从方式后,就可以用代码来实现了
Python实现
我们使用AVL对上一篇文章中的有序映射进行优化
因为AVL依赖于节点的高度,所以首先要重写一下Node类:
class AvlTree(OrderedMap):
class Node(OrderedMap.Node):
def __init__(self, element, parent=None, left=None, right=None):
super().__init__(element,parent,left,right)
self.height = 0
def left_height(self):
return self.left.height if self.left is not None else 0
def right_height(self):
return self.right.height if self.right is not None else 0
接下来定义4中调整的非公开方法
def _left_left(self,p):
this = p.node # 有变化的就4个节点
left = this.left
parent = this.parent
left_right = this.left.right
if parent is not None:
if this is parent.left:
parent.left = left
else:
parent.right = left
else:
self._root = left
this.parent = left
left.parent = parent
this.left = left_right
left.right = this
if left_right is not None:
left_right.parent = this
def _right_right(self,p):
this = p.node # 有变化的就4个节点
right = this.right
parent = this.parent
right_left = this.right.left
if parent is not None:
if this is parent.left:
parent.left = right
else:
parent.right = right
else:
self._root = right
this.parent = right
right.parent = parent
this.right = right_left
right.left = this
if right_left is not None:
right_left.parent = this
def _left_right(self,p):
self._right_right(self.left(p))
self._left_left(p)
def _right_left(self,p):
self._left_left(self.right(p))
self._right_right(p)
然后是用于平衡二叉树的方法,也就是根据情况调用上边那4种策略
def _isbalanced(self,p):
"""判断节点是否平衡"""
return abs(p.node.left_height() - p.node.right_height()) <= 1
def _recompute_height(self,p):
"""重新计算高度"""
p.node.height = 1 + max(p.node.left_height(),p.node.right_height())
def _rebalanced(self,p):
while p is not None:
if self._isbalanced(p):
self._recompute_height(p)
p = self.parent(p)
else:
if p.node.left_height()>p.node.right_height() and p.node.left.left_height()>p.node.left.right_height():
# LL的情况,只有自己和左孩子的高度可能变化
self._left_left(p)
elif p.node.right_height()>p.node.left_height() and p.node.right.right_height()>p.node.right.left_height():
# RR的情况,只有自己和右孩子的高度可能变化
self._right_right(p)
elif p.node.left_height()>p.node.right_height() and p.node.left.left_height()<p.node.left.right_height():
# LR的情况,只有自己和左孩子和左孩子的右孩子的高度可能变化
left = self.left(p)
self._left_right(p)
self._recompute_height(left)
else:
# RL的情况,只有自己和右孩子和右孩子的左孩子的高度可能变化
right = self.right(p)
self._right_left(p)
self._recompute_height(right)
while p is not None:
# 调整所有p的祖先的高度
self._recompute_height(p)
p = self.parent(p)
然后把方法封装成删除时和插入时的两个方法,虽然执行的内容是相同的
def _rebalanced_insert(self,p):
"""插入时的平衡调整"""
self._rebalanced(p)
def _rebalanced_delete(self, p):
"""删除时的平衡调整"""
self._rebalanced(p)
最后重写一下setitem方法与删除时调用的方法
def __setitem__(self, k, v):
"""优化setitem"""
if self.is_empty():
leaf = self.add_root(self._Item(k, v))
else:
p = self._subtree_search(self.root(), k)
if p.key() == k:
p.element().value = v
return
else:
item = self._Item(k, v)
if p.key() < k:
leaf = self.add_right(p, item)
else:
leaf = self.add_left(p, item)
self._rebalanced_insert(leaf)
def mapdelete(self, p):
if self.left(p) and self.right(p): # 两个孩子都有的时候
replacement = self._subtree_last_position(
self.left(p)) # 用左子树最右位置代替
self.replace(p, replacement.element())
p = replacement
parent = self.parent(p)
self.delete(p)
self._rebalanced_delete(parent)
在实现4种平衡策略时,一定要记着将整棵树的根节点更新,不然遍历的时候,根节点指的就不是真正的根节点了。
详细理解平衡二叉树AVL与Python实现的更多相关文章
- 数据结构与算法--从平衡二叉树(AVL)到红黑树
数据结构与算法--从平衡二叉树(AVL)到红黑树 上节学习了二叉查找树.算法的性能取决于树的形状,而树的形状取决于插入键的顺序.在最好的情况下,n个结点的树是完全平衡的,如下图"最好情况&q ...
- 平衡二叉树AVL - 插入节点后旋转方法分析
平衡二叉树 AVL( 发明者为Adel'son-Vel'skii 和 Landis)是一种二叉排序树,其中每一个节点的左子树和右子树的高度差至多等于1. 首先我们知道,当插入一个节点,从此插入点到树根 ...
- iOS - 详细理解KVC与KVO
详细理解KVC与KVO 在面试的时候,KVC与KVO有些时候还是会问到的,并且他们都是Objective C的关键概念,在这里我们先做一个简单地介绍: (一)KVC: KVC即指:NSKeyValue ...
- 二叉查找树(BST)、平衡二叉树(AVL树)(只有插入说明)
二叉查找树(BST).平衡二叉树(AVL树)(只有插入说明) 二叉查找树(BST) 特殊的二叉树,又称为排序二叉树.二叉搜索树.二叉排序树. 二叉查找树实际上是数据域有序的二叉树,即对树上的每个结点, ...
- AVL树Python实现
# coding=utf-8 # AVL树Python实现 def get_height(node): return node.height if node else -1 def tree_mini ...
- 史上最详细的C语言和Python的插入排序算法
史上最详细的C语言和Python的插入排序算法插入排序原理:所谓插入排序,就像我们在打牌(斗地主)时,整理我们自己手中自己的牌一样,就像是2,1,3,9,J,K,5,4,这四张牌.我们要把它其中的几张 ...
- [转帖]linux中systemctl详细理解及常用命令
linux中systemctl详细理解及常用命令 2019年06月28日 16:16:52 思维的深度 阅读数 30 https://blog.csdn.net/skh2015java/article ...
- 二叉查找树、平衡二叉树(AVL)、B+树、联合索引
1. [定义] 二叉排序树(二拆查找树)中,左子树都比节点小,右子树都比节点大,递归定义. [性能] 二叉排序树的性能取决于二叉树的层数 最好的情况是 O(logn),存在于完全二叉排序树情况下,其访 ...
- linux中systemctl详细理解及常用命令
linux中systemctl详细理解及常用命令 https://blog.csdn.net/skh2015java/article/details/94012643 一.systemctl理解 Li ...
随机推荐
- 第4章 Selenium2-java WebDriver API (三)
4.12 上传文件 4.12.1 sendKeys实现上传 html <html> <head> </head> <body> <div ...
- Baffle.js – 用于实现文本模糊效果的 JavaScript 库
Baffle.js 是一个 JavaScript 库,设计用来模糊和揭开DOM元素的文本. 这些元素可以是一个 CSS 选择器的形式.一个节点列表或者一个单节点. 你也可以传递一个选择对象给插件. 在 ...
- U3D GameObject 解读
GameObject本身没有功能,是Unity场景里所有组件的基类,但很多时候我们需要在脚本中操作GameObject.先讲一下GameObject类包含哪些内容,其中常用的用红色标出了 Variab ...
- 偏流角(Draft Angle)在等距螺旋中的作用
劳动改变人,思维改变世界.我们可以接着聊螺旋线了. 在飞行程序设计中,偏流角(Draft Angle简写为DA)通常指得是受侧风影响航向偏移的最大角度.用速度向量来表示时,是图1中的三角形关系: 图1 ...
- flask数据库迁移理解及命令
前言: 使用数据库迁移,可以直接建表,而不用我们自己写sql语句用来建表.就是将关系型数据库的一张张表转化成了Python的一个个类. 在开发中经常会遇到需要修改原来的数据库模型,修改之后更新数据库, ...
- [SCOI2010] 连续攻击游戏
题目 Description lxhgww最近迷上了一款游戏,在游戏里,他拥有很多的装备,每种装备都有2个属性,这些属性的值用[1,10000]之间的数表示.当他使用某种装备时,他只能使用该装备的某一 ...
- [转]VR原理讲解及开发入门
本文转自:http://www.52vr.com/article-661-1.html 本文是作者obuil根据多年心得专门为想要入门的VR开发者所写,由52VR网站提供支持. 1. VR沉浸感和 ...
- dotnet core 编程规范
本文实际只是翻译 .NET Core foundational libraries 官方文档的编码风格 在 .NET Core foundational libraries项目使用的编程规范默认就是 ...
- c# 获取客户端文件
/// <summary> /// 获取有效客户端文件控件集合,文件控件必须上传了内容,为空将被忽略, /// 注意:Form标记必须加入属性 enctype="multipar ...
- 使用Maven搭建JFinal环境
使用Maven搭建JFinal环境 工具:IDEA 2017 JFinal版本:3.4 一.Maven项目创建 选择maven模板进行创建 填写GroupId和ArtifactId 一路Next即可 ...