数据结构与算法-基础(十一)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指的是任意一个节点拥有的子节点个数,所以我 ...
随机推荐
- 剑指 Offer 34. 二叉树中和为某一值的路径
剑指 Offer 34. 二叉树中和为某一值的路径 输入一棵二叉树和一个整数,打印出二叉树中节点值的和为输入整数的所有路径.从树的根节点开始往下一直到叶节点所经过的节点形成一条路径. 示例: 给定如下 ...
- 使用kubeadm安装kubernetes 1.21
文章原文 配置要求 至少2台 2核4G 的服务器 本文档中,CPU必须为 x86架构 CentOS 7.8 或 CentOS Stream 8 安装后的软件版本为 Kubernetes v1.21.x ...
- ELK+kafka+filebeat搭建生产ELFK集群
文章原文 ELK 架构介绍 集群服务版本 服务 版本 java 1.8.0_221 elasticsearch 7.10.1 filebeat 7.10.1 kibana 7.10.1 logstas ...
- vue2.0与3.0中的provide和inject 用法
1.provide/inject有什么用? 常用的父子组件通信方式都是父组件绑定要传递给子组件的数据,子组件通过props属性接收,一旦组件层级变多时,采用这种方式一级一级传递值非常麻烦,而且代码可读 ...
- Linux下scp命令
1.scp 是 secure copy 的缩写, scp 是 linux 系统下基于 ssh 登陆进行安全的远程文件拷贝命令. 2.scp 是加密的,rcp 是不加密的,scp 是 rcp 的加强版. ...
- 第04课:使用 VS 管理开源项目
本节课将介绍 Redis 项目在 Linux 系统中使用 gdb 去调试,这里的调试环境是 CentOS 7.0,但是通常情况下对于 C/C++ 项目我一般习惯使用 Visual Studio 去做项 ...
- PHP的可变变量与可变函数
什么叫可变.在程序世界中,可变的当然是变量.常量在定义之后都是不可变的,在程序执行过程中,这个常量都是不能修改的.但是变量却不同,它们可以修改.那么可变变量和可变函数又是什么意思呢?很明显,就是用另一 ...
- shell脚本中 /dev/null 的用途
/dev/null 是一个特殊的设备文件,它丢弃一切写入其中的数据 可以将它 视为一个黑洞, 它等效于只写文件, 写入其中的所有内容都会消失, 尝试从中读取或输出不会有任何结果,同样,/dev/nul ...
- PHP 相对路径转换为绝对路径 realpath
* 相对路径 -> 绝对路径 realpath <?php /** * @param string $in_rel: relative directory * @param string ...
- ES增删改查
了解了一下python对es 7.5的操作,记录下,不难: #!/usr/bin/env python # -*- coding: UTF-8 -*- from settings import Con ...