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. 简单三分钟,本地搭建 k8s

    使用 minikube 在本地搭建 k8s 已经比以前要简单很多了.本文,我们通过简短的三分钟来重现一下在本地搭建 k8s 实验环境的步骤. Newbe.Claptrap 是一个用于轻松应对并发问题的 ...

  2. 基于Bootstrap v4.1.1 & Bootstrap-table-1.14.1实现数据瀑布流

    基于Bootstrap-table-1.14.1实现数据瀑布流 HTML代码 <div id="AvgWaitAndAvgTimeServiceTimeData_hall"& ...

  3. 使用Mosquitto实现MQTT客服端C语言

      上一篇文章已经将mosquitto移植到了arm平台上,现在将使用mosquitto完成mqtt客服端的demo,了解过mqtt协议的小伙伴都知道,mqtt主要分为代理服务器.发布者.订阅者三部分 ...

  4. Vue配置axios

    1)安装插件(一定要在项目目录下): >: cnpm install axios 2)在main.js中配置: import axios from 'axios' Vue.prototype.$ ...

  5. jQuery判断多种数据类型

    1.判断是否为数组类型 var obj=[0]; alert((typeof obj=='object')&&obj.constructor==Array)   2. 判断是否为字符串 ...

  6. 宝塔面板使用PM2命令提示Command Not Found解决方案

    1.查看node版本 进入/www/server/nvm/versions/node 查看node版本 2.复制以下代码 以node版本v12.18.1举例 PATH=$PATH:/www/serve ...

  7. LeetCode2-链表两数和

    目录 LeetCode2-链表两数和 题目描述 示例提示 经验教训 参考正解 题目描述 示例提示 经验教训 链表题的判空条件不是万能的,有时候示例会极其复杂,根本难以通过判空来区分不同情况. /** ...

  8. 第一次接触linux系统的你,必须要知道的概念

    linux系统一切皆为文件 linux系统一个多用户系统 没有消息就是好消息 linux系统目录结构 Linux文件系统采用带链接的树形目录结构,即只有一个根目录(通常用"/"表示 ...

  9. Laravel [1045] Access denied for user 'homestead'@'localhost' .env没有配置

    laravel 连接数据库出现错误 PDOException in Connector.php line 55:SQLSTATE[HY000] [1045] Access denied for use ...

  10. css颜色字符串转换, 字符串转化为驼峰形式

    * 将 rgb 颜色字符串转换为十六进制的形式,如 rgb(255, 255, 255) 转为 #ffffff1. rgb 中每个 , 后面的空格数量不固定2. 十六进制表达式使用六位小写字母3. 如 ...