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 节点的变化情况,并在更改节点之后,更新 gp 的高度。

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; /**
旋转之后,更改节点的指向,方法代码看下面
*/
}

做完旋转之后,要更改 gchild 节点的 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 树的更多相关文章

  1. 数据结构与算法(九):AVL树详细讲解

    数据结构与算法(一):基础简介 数据结构与算法(二):基于数组的实现ArrayList源码彻底分析 数据结构与算法(三):基于链表的实现LinkedList源码彻底分析 数据结构与算法(四):基于哈希 ...

  2. 数据结构与算法——平衡二叉树(AVL树)

    目录 二叉排序树存在的问题 基本介绍 单旋转(左旋转) 树高度计算 旋转 右旋转 双旋转 完整代码 二叉排序树存在的问题 一个数列 {1,2,3,4,5,6},创建一颗二叉排序树(BST) 创建完成的 ...

  3. 数据结构图文解析之:AVL树详解及C++模板实现

    0. 数据结构图文解析系列 数据结构系列文章 数据结构图文解析之:数组.单链表.双链表介绍及C++模板实现 数据结构图文解析之:栈的简介及C++模板实现 数据结构图文解析之:队列详解与C++模板实现 ...

  4. 数据结构与算法--从平衡二叉树(AVL)到红黑树

    数据结构与算法--从平衡二叉树(AVL)到红黑树 上节学习了二叉查找树.算法的性能取决于树的形状,而树的形状取决于插入键的顺序.在最好的情况下,n个结点的树是完全平衡的,如下图"最好情况&q ...

  5. Java数据结构和算法(一)树

    Java数据结构和算法(一)树 数据结构与算法目录(https://www.cnblogs.com/binarylei/p/10115867.html) 前面讲到的链表.栈和队列都是一对一的线性结构, ...

  6. 数据结构与算法系列研究五——树、二叉树、三叉树、平衡排序二叉树AVL

    树.二叉树.三叉树.平衡排序二叉树AVL 一.树的定义 树是计算机算法最重要的非线性结构.树中每个数据元素至多有一个直接前驱,但可以有多个直接后继.树是一种以分支关系定义的层次结构.    a.树是n ...

  7. Java数据结构和算法(二)树的基本操作

    Java数据结构和算法(二)树的基本操作 数据结构与算法目录(https://www.cnblogs.com/binarylei/p/10115867.html) 一.树的遍历 二叉树遍历分为:前序遍 ...

  8. 数据结构54:平衡二叉树(AVL树)

    上一节介绍如何使用二叉排序树实现动态查找表,本节介绍另外一种实现方式——平衡二叉树. 平衡二叉树,又称为 AVL 树.实际上就是遵循以下两个特点的二叉树: 每棵子树中的左子树和右子树的深度差不能超过 ...

  9. java数据结构和算法07(2-3-4树)

    上一篇我们大概了解了红黑树到底是个什么鬼,这篇我们可以看看另外一种树-----2-3-4树,看这个树的名字就觉得很奇怪.... 我们首先要知道这里的2.3.4指的是任意一个节点拥有的子节点个数,所以我 ...

随机推荐

  1. ReScript 与 TypeScript,谁是前端圈的“当红辣子鸡”

    摘要: ReScript 和 TypeScript 的出现都是为了更好地使用JavaScript,但两者还是有很大的不同. 本文分享自华为云社区<[云创共驻]ReScript 和 TypeScr ...

  2. 【C++周报】第二期 2021-8-19

    这次我们照样看一道题.个人认为比上一次的简单. https://vijos.org/p/1130 先说方法,动态规划,你能想到什么? "在它的左边加上一个自然数,但该自然数不能超过原数的一半 ...

  3. Markdown公式用法大全

    目录 基本语法 两种代码引用方式 插入链接并描述 插入图片 有序列表 无序列表 分割线 表格 如何插入公式 如何输入上下标 如何输入括号和分隔符 如何输入分数 如何输入开方 如何输入省略号 如何输入矢 ...

  4. 关于php的ini文件相关操作函数浅析

    在小公司,特别是创业型公司,整个服务器的搭建一般也是我们 PHP 开发工程师的职责之一.其中,最主要的一项就是要配置好服务器的 php.ini 文件.一些参数会对服务器的性能产生深远的影响,而且也有些 ...

  5. composer 下载包慢

    方法一: 修改 composer 的全局配置文件(推荐方式) 打开命令行窗口(windows用户)或控制台(Linux.Mac 用户)并执行如下命令: composer config -g repo. ...

  6. Shell系列(3)- 命令别名

    前言 使用alias命令创建命令别名,是Bash的一个基本功能:别名有两种形式,一种暂时的,Linux重启后失效.另外一种永久的通过该配置文件实现 使用更改别名 临时 命令格式:alias 别名='原 ...

  7. Java线程类

    基础知识 线程状态 根据Thread.State类中的描述,Java中线程有六种状态:NEW,RUNNABLE,WAITING,TERMINATED,BLOCKED. 就绪状态(NEW):当线程对象调 ...

  8. requestAnimationFrame 切换页面问题

    requestAnimationFrame 切换页面时, 之前定时的内容还会继续执行. 所以 要注意处理动画函数内容,否则会出现死循环. 遇到的问题: 我在两个页面都有使用 requestAnimat ...

  9. 浅聊Linux的五种IO模型

    在日常 Coding 中,多多少少都会接触到网络 IO,就会想要深入了解一下.看了很多文章,总是云里雾里的感觉,直到读了<UNIX网络编程 卷1:套接字联网API>中的介绍后,才豁然开朗. ...

  10. Nresource服务之接口缓存化

    1. 背景 Nresource服务日均4.5亿流量,考虑到未来流量急增场景,我们打算对大流量接口进行缓存化处理:根据服务管理平台数据统计显示getUsableResoureCount接口调用量很大,接 ...