数据结构与算法-基础(十一)AVL 树
AVL 树 是最早时期发明的自平衡二叉搜索树之一。是依据它的两位发明者的名称命名。
AVL 树有一个重要的属性,即平衡因子(Balance Factor),平衡因子 == 某个节点的左右子树高度差。
AVL 树特点总结下来有:
- 每个节点的平衡因子有且仅有 1、0、-1,若超过这三个值的范围,就称其为失衡;
- 每个节点左右子树的高度差不会超过 1;
- 搜索、添加、删除的时间复杂度为 O(logn),n 为 n 个节点。

看上图,右侧图中二叉树就可以称为AVL 树。
添加后导致失衡
若再添加一个元素 18,导致它的祖先节点失衡(甚至有可能它的所有祖父节点都失衡)。但是的父节点和非祖先节点不会失衡。

当添加后导致失衡,就要想办法调整节点,使其再达到平衡状态。前提是,必须要满足二叉搜索树的性质,不能随意调整。这里调整方法就是旋转节点。
旋转节点
旋转节点核心调整祖父节点和父节点的位置,使得其调整后的节点达到平衡,在操作的过程中类似旋转,所以称为旋转节点。
先确定几个节点的简称,方便下面的说明:
- g:祖父节点
- p:父节点
- n:自身节点
LL - 右旋转(单旋)
LL: 平衡因子小于 -1 的节点。该失衡的节点为 g,它的左子树为 p,p 的左子树为 n。因为 g、p 、n 都在左侧,所以称为 LL,如下图,当添加元素 1 之后,造成失衡。

当出现 LL 类型的失衡时,可以通过右旋转达到平衡,如下图:

从图中可以看出,相当于将 g、p、n 三个节点向右侧旋转,达到平衡,即:
g.left = p.right
p.right = g
若节点发生变化,需要注意节点 parent 节点的变化情况,并在更改节点之后,更新 g 和 p 的高度。
RR - 左旋转(单旋)
RR 的情况和 LL 情况正好相反,所以旋转也是相反的,旋转之后的处理和 LL 是一样的,这个可以在后面的代码中体现处理,所以不再重复去解释这种情况。
LR - RR 左旋转,LL 右旋转(双旋)
LR : 比如添加元素 4 后失衡,的失衡节点定为 g,它的左子树为 p,p 的右子树为 n。因为 p 在左,n 在右。所以这种情况被称为 LR。

出现 LR 情况需要先对 p 节点左旋转处理,使其变成 LL 的情况之后,再对 g 做右旋转,恢复平衡状态。

注意:图中标识错误,是 p 向左旋转,g 向右旋转
每当旋转后都要更新其对应节点的高度。
RL - LL 右旋转,RR 左旋转(双旋)
RL 的情况与 LR 的情况也是正好相反的。处理上也是需要反着来就好。旋转后也是一定要更新其对应的节点。
实现代码
这里以 RR 为例,RR 的情况就需要进行左旋转。
void rotateLeft(Node<E> grand) {
// 创建临时变量 parent 和 child
Node<E> parent = grand.right;
Node<E> child = parent.left;
// 祖父节点的右子节点为父节点的左子节点
grand.right = child;
// 父节点的左节点为祖父节点
parent.left = grand;
/**
旋转之后,更改节点的指向,方法代码看下面
*/
}
做完旋转之后,要更改 g 和 child 节点的 parent 指向。
void afterRotate(Node<E> grand, Node<E> parent, Node<E> child) {
// 调整 grand 的 parent 节点
if (grand.isLeftChild()) {
grand.parent.left = parent;
} else if (grand,isRightChild()) {
grand.parent.right = parent;
} else {
root = parent;
}
if (child != null) {
child.parent = grand;
}
parent.parent = grand.parent;
grand.parent = parent;
/**
调整之后,更新 g 和 p 的高度,方法代码看下面
*/
}
这里是在每个 AVL 节点中定义一个 height 属性,来记录当前节点的高度,所以可以在 AVL 节点中实现更新高度的方法。
public void updateHeight() {
int leftHeight = left == null ? 0 : ((AVLNode<E>)left).height;
int rightHeight = right == null ? 0 : ((AVLNode<E>)right).height;
height = 1 + Math.max(leftHeight, rightHeight);
}
当前节点的高度就是它的左右子节点的高度中的最大值。代码逻辑如上
这时候,就有可能有个问题,怎么保证 AVL 节点中的 height 都是一一对应的呢?这里是在添加方法之后做的处理,核心逻辑就是在每次添加一个节点之后就更新下全部的节点高度就可以,详细的在下面去深究。
有了上面左旋转的实现,那么右旋转的实现也就能顺利得到,就是和左旋转相反就好。
private void rotateRight(Node<E> grand) {
Node<E> parent = grand.left;
Node<E> child = parent.right;
grand.left = child;
parent.right = grand;
afterRotate(grand, parent, child);
}
知道了左右旋转和旋转后更新节点的方法之后,接下来就要来判断失衡的节点,以及失衡的情况是 LL 还是 RR,或者其他。
首先判断失衡的节点,判断一个节点是否平衡,就要看这个节点的平衡因子是否是在 -1 到 1 之间的,即
boolean isBalanced(Node<E> node) {
return Math.abs(((AVLNode<E>)node).balanceFactor()) <= 1;
}
public int balanceFactor() {
int leftHeight = left == null ? 0 : ((AVLNode<E>)left).height;
int rightHeight = right == null ? 0 : ((AVLNode<E>)right).height;
return leftHeight - rightHeight;
}
添加节点
当添加一个节点时,因为当前节点是没有左右子节点的,所以要判断的不是当前节点,而是这个节点的父节点祖父节点们是否失衡,这就用到循环遍历
void afterAdd(Node<E> node) {
while ((node = node.parent) != null) {
if (isBalanced(node)) {
// 更新高度
updateHeight(node);
}
else {
// 恢复平衡
rebalance(node);
break;
}
}
}
看上面代码中,有一个恢复平衡的方法没有说过,在调用这个方法时,就已经确定这个节点是失衡状态,这时要做的就是确定失衡的情况,然后根据不同的情况做相应的旋转处理。
这里要先知道节点的最大高度,就是左右子节点高度中的最大值,即
public Node<E> tallerChild() {
int leftHeight = left == null ? 0 : ((AVLNode<E>)left).height;
int rightHeight = right == null ? 0 : ((AVLNode<E>)right).height;
if (leftHeight > rightHeight) return left;
if (leftHeight < rightHeight) return right;
return isLeaf() ? left : right;
}
高度大的节点对应到要处理的节点,可看下面代码:
void rebalance(Node<E> grand) {
Node<E> parent = ((AVLNode<E>)grand).tallerChild();
Node<E> node = ((AVLNode<E>)parent).tallerChild();
if (parent.isLeftChild()) { // L
if (node.isLeftChild()) { // LL
rotateRight(grand);
}
else { // LR
rotateLeft(parent);
rotateRight(grand);
}
}
else { // R
if (node.isLeftChild()) { // RL
rotateRight(parent);
rotateLeft(grand);
}
else { // RR
rotateLeft(grand);
}
}
}
删除节点
当删除节点后,也可能导致 AVL 树失衡,有可能是父节点或者祖先节点失衡,通过添加节点的处理失衡逻辑后,可以总结删除节点的失衡处理逻辑,也是先要遍历查找失衡的节点,然后把这个节点的失衡情况搞出来,最后对应情况做旋转处理。
所以删除后的处理就是:
void afterRemove(Node<E> node) {
while ((node = node.parent) != null) {
if (isBalanced(node)) {
// 更新高度
updateHeight(node);
}
else {
// 恢复平衡
rebalance(node);
}
}
}
总结
看代码,添加或者删除节点之后都要一一的遍历节点的父节点和祖先节点,没有失衡的更新高度,失衡的就恢复平衡,知道 root 节点,为什么这样?
- 添加节点后,可能会导致所有的祖先节点都失衡
- 删除节点后,可能会导致父节点或祖先接单失衡,恢复平衡后,可能会导致更高层次的祖先节点失衡
所以就需要整体的遍历到 root 节点。单处理一个节点是不够的。
时间复杂度上,搜索是 O(logn),添加是 O(logn),只需要 O(1) 次的旋转,删除也是 O(logn),最多需要 O(logn) 次的旋转。
数据结构与算法-基础(十一)AVL 树的更多相关文章
- 数据结构与算法(九):AVL树详细讲解
数据结构与算法(一):基础简介 数据结构与算法(二):基于数组的实现ArrayList源码彻底分析 数据结构与算法(三):基于链表的实现LinkedList源码彻底分析 数据结构与算法(四):基于哈希 ...
- 数据结构与算法——平衡二叉树(AVL树)
目录 二叉排序树存在的问题 基本介绍 单旋转(左旋转) 树高度计算 旋转 右旋转 双旋转 完整代码 二叉排序树存在的问题 一个数列 {1,2,3,4,5,6},创建一颗二叉排序树(BST) 创建完成的 ...
- 数据结构图文解析之:AVL树详解及C++模板实现
0. 数据结构图文解析系列 数据结构系列文章 数据结构图文解析之:数组.单链表.双链表介绍及C++模板实现 数据结构图文解析之:栈的简介及C++模板实现 数据结构图文解析之:队列详解与C++模板实现 ...
- 数据结构与算法--从平衡二叉树(AVL)到红黑树
数据结构与算法--从平衡二叉树(AVL)到红黑树 上节学习了二叉查找树.算法的性能取决于树的形状,而树的形状取决于插入键的顺序.在最好的情况下,n个结点的树是完全平衡的,如下图"最好情况&q ...
- Java数据结构和算法(一)树
Java数据结构和算法(一)树 数据结构与算法目录(https://www.cnblogs.com/binarylei/p/10115867.html) 前面讲到的链表.栈和队列都是一对一的线性结构, ...
- 数据结构与算法系列研究五——树、二叉树、三叉树、平衡排序二叉树AVL
树.二叉树.三叉树.平衡排序二叉树AVL 一.树的定义 树是计算机算法最重要的非线性结构.树中每个数据元素至多有一个直接前驱,但可以有多个直接后继.树是一种以分支关系定义的层次结构. a.树是n ...
- Java数据结构和算法(二)树的基本操作
Java数据结构和算法(二)树的基本操作 数据结构与算法目录(https://www.cnblogs.com/binarylei/p/10115867.html) 一.树的遍历 二叉树遍历分为:前序遍 ...
- 数据结构54:平衡二叉树(AVL树)
上一节介绍如何使用二叉排序树实现动态查找表,本节介绍另外一种实现方式——平衡二叉树. 平衡二叉树,又称为 AVL 树.实际上就是遵循以下两个特点的二叉树: 每棵子树中的左子树和右子树的深度差不能超过 ...
- java数据结构和算法07(2-3-4树)
上一篇我们大概了解了红黑树到底是个什么鬼,这篇我们可以看看另外一种树-----2-3-4树,看这个树的名字就觉得很奇怪.... 我们首先要知道这里的2.3.4指的是任意一个节点拥有的子节点个数,所以我 ...
随机推荐
- 性能测试工具JMeter 基础(一)—— 安装、配置环境变量
JMeter下载 下载地址:https://jmeter.apache.org/download_jmeter.cgi 下载完成后解压后可直接使用,不用进行安装 环境变量配置 新增变量名:JMETER ...
- Django——Auth模块(用户认证模块)
1.Auth模块简介 auth模块是对登录认证方法的一种封装,之前我们获取用户输入的用户名及密码后需要自己从user表里查询有没有用户名和密码符合的对象. 而有了auth模块之后就可以很轻松的去验证用 ...
- Python图像分割之区域增长法
原文链接:https://blog.csdn.net/sgzqc/article/details/119682864 一.简介 区域增长法是一种已受到计算机视觉界十分关注的图像分割方法.它是以区域为处 ...
- Identity角色管理一(准备工作)
因角色管理需要有用户才能进行(需要将用户从角色中添加,删除)故角色管理代码依托用户管理 只需在Startup服务中添加角色管理即可完成 public void ConfigureServices(IS ...
- call、apply、bind三者比较
var obj={a:1}; var foo={ getA:function(item1,item2){ return this.a+item1+item2 } } // apply绑定参数为数组,一 ...
- json包中的Marshal&Unmarshal 文档译本
Marshal func Marshal(v interface{})([]byte, error) Marshal returns the JSON encoding of v. Marshal返回 ...
- FastDFS 配置 Nginx 模块及访问测试
#备注:以下nginx-1.10.3源码目录根据nginx版本号不同会有相应的变化,以nginx版本号为准#一.安装 Nginx 和 fastdfs-nginx-module1,安装 Nginx 请看 ...
- 并发编程之:ForkJoin
大家好,我是小黑,一个在互联网苟且偷生的农民工. 在JDK1.7中引入了一种新的Fork/Join线程池,它可以将一个大的任务拆分成多个小的任务并行执行并汇总执行结果. Fork/Join采用的是分而 ...
- 九、Abp vNext 基础篇丨评论聚合功能
介绍 评论本来是要放到标签里面去讲的,但是因为上一章东西有点多了,我就没放进去,这一章单独拿出来,内容不多大家自己写写就可以,也算是对前面讲解的一个小练习吧. 相关注释我也加在代码上面了,大家看看代码 ...
- Java基础系列(8)- 数据类型
数据类型 强类型语言 要求变量的使用合乎规定,所有的变量都必须先定义才能使用.Java是强类型语言. 弱类型语言 变量定义比较随意,比如"12"+3,可以是int型123,也可以是 ...