封面画师:ツチヤ     封面ID:79898339

本文参考视频:小马哥教育(SEEMYGO) 2019年 恋上数据结构与算法(第一季)

源码仓库:mofan212/data-structure-and-algorithm (github.com)

辅助学习网址:数据结构和算法动态可视化

0. 前言

二叉搜索树的复杂度分析

如果是按照7、4、9、2、5、8、11的顺序添加节点:

一棵二叉搜索树

如果我们想要添加一个节点 12 ,那么我们需要先和根节点 7 进行比较,然后和其子节点 8 进行比较,最后在和 8 的子节点 11 进行比较,确定插入位置。同理,如果我们要添加节点 1 或 6 也是同样的道理。

不难发现,二叉搜索树的复杂度与元素(节点)的个数无关,与树的高度有关。

因此,上述二叉搜索树(满二叉树)的复杂度为: O(h) = O(logn) ,可以看出二叉搜索树的添加、查找效率很高。

❗但是!

如果我们更换添加节点的顺序,将添加节点的顺序变成从小到大添加:

奇怪的二叉搜索树

这时,这棵二叉搜索树的复杂度为: O(h) = O(n) ,二叉搜索树退化成链表


我们发现,当二叉树的节点数量 n 较大时,两者的性能差异比较大。

比如 n = 1000000 时,二叉搜索树的最低高度是 20 ,但最大高度是 1000000 ,这两种情况很极端,会导致其搜索效率出现很大的差别。


添加节点顺序不当可能会造成二叉搜索树退化成链表,删除节点时,也可能导致二叉搜索树退化成链表。比如我删除最开始的二叉树中的 2 、8、11、9 四个节点时:

退化为链表的BST

综上可知,添加、删除节点时,都可能会导致二叉搜索树退化成链表。

那么,我们有什么办法防止二叉搜索树退化成链表吗? 让添加、删除、搜索的复杂度维持在O(logn)

这时,又一个“天降猛男”登场了 —— 平衡二叉搜索树。💪

1 平衡二叉搜索树

1.1 平衡

平衡(Balance): 当节点数量固定时,左右子树的高度越接近,这棵二叉树就越平衡(高度越低)。

平衡

上图所示的二叉树越来越平衡。

这里又可以引入理想平衡的概念:最理想的平衡,就是像完全二叉树、满二叉树一样,高度是最小的。

1.2 改进二叉搜索树

前面已经分析:节点的添加、删除可能会导致BST退化成链表,但是因为节点的添加、删除是无限制的,基本是随机的,因此我们并不能在节点的添加和删除上天机限制。

但是我们依然有改进方案: 在节点的添加、删除之后,想办法让二叉搜索树恢复平衡(减小树的高度)。

调整二叉搜索树

我们还可以继续调整节点的位置,并完全可以达到理想平衡,但是付出的代价可能会比较大。由于增加了调整的次数,反而会增加时间复杂度,甚至超过链表的时间复杂度而得不偿失。

总的来说,我们不能一昧地调整,我们用尽量少的调整次数让BST达到适度的平衡即可

一棵达到适度平衡的BST可以称之为: 平衡二叉搜索树。

1.3 平衡二叉搜索树

平衡二叉搜索树,Balanced Binary Search Tree,简称:BBST 。经典的平衡二叉搜索树有:

  • AVL树
    • 在Windows NT内核中广泛使用
  • 红黑树
    • C++ STL (比如map、set等)
    • Java中 的 TreeMap、TreeSet、HashMap、HashSet
    • Linux 的进程调度
    • Nginx 的 timer 管理

一般也称它们为: 自平衡的二叉搜索树 (Self-balancing Binary Search Tree

2. AVL树

2.1 基本概念

AVL树是最早发明的自平衡二叉搜索树,那么AVL是什么意思? AVL 取名于两位发明者的名字:

G. M. Adelson-Velsky 和 E. M. Landis ,这是两位来自苏联的科学家(“精苏”狂喜😜)

AVL ? 艾薇儿? 艾薇儿树? 嗯! 可以!😂

Avril

平衡因子

平衡因子(Balance Factor):某节点的左右子树高度差子树的高度减子树的高度)。

比如:

平衡因子举例

对于AVL树来说,AVL树每个节点的平衡因子只可能是 1、0、-1(绝对值小于等于1,如果超过1,称之为“失衡”),换句话说,AVL树每个节点的左右子树高度差不超过 1 。搜索、添加、删除的时间复杂度是O(logn)

AVL树示例:

AVL树示例

2.2 代码重构

为了便于代码复用,我们可以对编写的二叉搜索树代码进行重构,即:编写一个BinaryTree类,用来表示所有二叉树都具有的一些公共方法和成员变量;将原来表示二叉搜索树的类BinarySearchTree改写为BST,同时使用BST类继承BinaryTree

BinaryTree中包含的一些方法和成员变量(省略方法的实现,具体实现参考 二叉搜索树 中给出的代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.yang.Tree;
import java.util.LinkedList;
import java.util.Queue;
public class BinaryTree<E>{
// 使用访问修饰符 protected 让不同包的子类可以访问这个变量
protected int size; // 节点个数
protected Node<E> root; //根节点
public int size() {
return size;
}

public boolean isEmpty() {...}

public void clear() {...}

// 前序
public void preorder(Visitor<E> visitor) {...}

private void preorder(Node<E> node,Visitor<E> visitor) {...}

// 中序
public void inorder(Visitor<E> visitor) {...}

private void inorder(Node<E> node, Visitor<E> visitor) {...}

// 后序
public void postorder(Visitor<E> visitor) {...}

private void postorder(Node<E> node, Visitor<E> visitor) {...}

public void levelOrder(Visitor<E> visitor) {...}

public boolean isComplete() {...}

public int height() {...}

// 获取整棵树的高度
public int height2() {...}

// 获取某一节点的高度
private int height(Node<E> node) {...}

protected Node<E> predecessor(Node<E> node) {...}

protected Node<E> successor(Node<E> node) {...}

public static abstract class Visitor<E> {
// 遍历终止标志 true:终止 false:全遍历
boolean stop;

// 如果返回true,表示停止遍历
abstract boolean visit(E element);
}
// 一个节点就是一个Node
protected static class Node<E> {...}
}

BST中包含的一些方法和成员变量(省略方法的实现,具体实现参考 二叉搜索树 中给出的代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.yang.Tree;
import java.util.Comparator;
public class BST<E> extends BinaryTree<E> {
private Comparator<E> comparator;

public BST() {
this(null);
}

public BST(Comparator<E> comparator) {
this.comparator = comparator;
}

public void add(E element) {...}

// 删除某一元素
public void remove(E element) {...}

public boolean contains(E element) {...}

// 删除某一节点
private void remove(Node<E> node) {...}

// 根据元素找寻节点
private Node<E> node(E element) {...}

/**
* @return 返回值等于0,表示e1和e2相等;返回值大于0,表示e1大于e2;返回值小于0,表示e1小于e2
*/
private int compare(E e1, E e2) {...}

// 节点空值检查
private void elementNotNullCheck(E element) {...}
}

AVL树是自平衡二叉搜索树,是一种特殊的二叉搜索树,因此在编写AVL树的代码时,我们可以让表示AVL树的类AVLTree继承BST。可以初步得到以下代码:

1
2
3
4
5
6
7
8
9
10
public class AVLTree<E> extends BST<E>{

public AVLTree() {
this(null);
}

public AVLTree(Comparator<E> comparator) {
super(comparator);
}
}

2.3 添加失衡

比如向下面这棵子树中添加元素 13 :

添加失衡

可以看到,原本这棵子树是平衡的,添加元素 13 后而变成了失衡。最坏情况下,甚至可能导致添加节点的所有祖先节点都失衡,但添加节点的父节点(节点 12 )、非祖先节点(节点 4、6、8)不会失衡。

那么怎么解决因为添加而导致的失衡呢? 需要使用到旋转。 👇

2.4 添加旋转

2.4.1 LL - 右旋转(单旋)

右旋转示意图:

添加右旋转

右旋转动态图:

右旋转

在上述示意图中,n 表示节点, p(parent)表示父节点,g(grandparent)表示祖父节点;T0、T1、T2、T3分别表示节点下的子树,其中 T0、T1 的高度相等,T2、T3 的高度相等。

最开始时,整棵子树是平衡的,当我们在 T0 子树出处插入一个节点(图中红色矩形)时,节点 g 处于失衡状态,我们需要对其进行操作,让它恢复平衡。

这时,需要对 g 用到右旋转 。右旋转步骤如下:

  • g.left = p.right
  • p.right = g
  • 让 p 成为这棵子树的根节点
  • 旋转后仍然是一棵二叉搜索树: T0 < n < T1 < p < T2 < g < T3
  • 整棵树都达到平衡

旋转结果如上图的右侧部分。明明是右旋转,为什么要叫LL呢?因为失去平衡的节点是由其左子树的左子树下的节点造成的。

并不是旋转了就完事了,我们还需要注意维护的内容:

  • T2、p、g 的parent属性
  • 先后更新 g 、p 的高度

2.4.2 RR - 左旋转(单旋)

左旋转示意图:

添加左旋转

左旋转动态图:

左旋转

在上述示意图中,n 表示节点, p(parent)表示父节点,g(grandparent)表示祖父节点;T0、T1、T2、T3分别表示节点下的子树,其中 T0、T1 的高度相等,T2、T3 的高度相等。

最开始时,整棵子树是平衡的,当我们在 T3 子树出处插入一个节点(图中红色矩形)时,节点 g 处于失衡状态,我们需要对其进行操作,让它恢复平衡。

这时,需要对 g 用到左旋转 。左旋转步骤如下:

  • g.right = p.left
  • p.left = g
  • 让 p 成为这棵子树的根节点
  • 仍然是一棵二叉搜索树: T0 < g < T1 < p < T2 < n < T3
  • 整棵树都达到平衡

旋转结果如上图的右侧部分。

并不是旋转了就完事了,我们还需要注意维护的内容:

  • T1、p、g 的 parent 属性
  • 先后更新 g、p 的高度

2.4.3 双旋

LR

遇上 LR 的情况(向T1、T2处添加节点),需要先对 p 进行 RR 左旋转,再对 g 进行 LL 右旋转(双旋)。过程如下:

添加LR双旋

RL

遇上 RL 的情况(向T1、T2处添加节点),需要先对 p 进行 LL 右旋转,再对 g 进行 RR 左旋转(双旋)。过程如下:

添加RL双旋

2.5 计算平衡因子

由于AVL树的特性,我们在添加元素时需要计算某一节点的平衡因子,并对失衡节点进行旋转,让其再次达到平衡状态,而这一步骤的前提就是需要计算平衡因子。

平衡因子就是某一节点左子树的高度减去右子树的高度 ,明白了这个道理后就很好计算了。

需要找到一种表示节点高度的方法,首先想到的就是在BinaryTree类中的静态嵌套类Node中添加表示高度的成员变量height 。但是这么做有一个问题,如果我创建的不是AVL树,只是普通的BST,那么这个时候也会开辟一段内存空间用来存储height,而BST中并没有用到它,造成空间被白白浪费。

我们可以在AVLTree类中编写表示AVL树自己节点的类AVLNode,让这个类继承Node并添加成员变量height,这样就可以做到创建AVL树或创建BST时,都有满足自身要求的节点。最后,就可以根据height来计算平衡因子并判断某一节点是否平衡:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 判断节点是否平衡
private boolean isBalanced(Node<E> node){
return Math.abs(((AVLNode<E>) node).balanceFactor()) <= 1;
}

private static class AVLNode<E> extends Node<E> {
// 新添加节点是叶子节点,高度一定是 1
int height = 1;

public AVLNode(E element, Node<E> parent) {
super(element, parent);
}

// 获取平衡因子
public int balanceFactor() {
int leftHeight = left == null ? 0 : ((AVLNode<E>) left).height;
int rightHeight = right == null ? 0 : ((AVLNode<E>) right).height;
return leftHeight - rightHeight;
}
}

AVLTree继承了BST,它俩使用相同的节点添加方法 ,但是我们知道AVL树和BST添加的节点是不一样的,AVL树添加的节点中有height表示高度,而BST是没有的。因为这个差异,我们需要对代码进行修改:

先在BinaryTree中编写节点添加方法:

1
2
3
4
// 访问修饰符 protected 让该类的子类可以使用
protected Node<E> createNode(E element, Node<E> parent){
return new Node<>(element, parent);
}

修改BST中的添加方法,使用createNode()方法创建节点,同时编写afterAdd()方法(该方法为空,在AVLTree进行重写)表示添加节点后需要做的调整:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public void add(E element) {
elementNotNullCheck(element);
// 添加第一个节点 (根节点)
if (root == null) {
// 使用新创建节点方法
root = createNode(element,null);
size++;
// 添加节点后的处理 传入参数类型为Node
afterAdd(root);
return;
}
// 添加的不是第一个节点 ---> 找到插入节点的父节点
Node<E> parent = root;
Node<E> node = root;
int cmp = 0;
while (node != null) {
cmp = compare(element, node.element);
parent = node; // 保存父节点
if (cmp > 0) {
node = node.right;
} else if (cmp < 0) {
node = node.left;
} else { // 相等
node.element = element; // 相等时覆盖
return;
}
}
// 看看插入到父节点的哪个位置
Node<E> newNode = createNode(element, parent); // 使用新创建节点方法
if (cmp > 0) {
parent.right = newNode;
} else {
parent.left = newNode;
}
size++;
// 添加节点后的处理 传入参数类型为Node
afterAdd(newNode);
}

// 添加节点之后的调整 node是新添加的节点
// 在其他代码中实现
protected void afterAdd(Node<E> node){ }

最后在AVLTree中重写createNode()方法:

1
2
3
4
@Override
protected Node<E> createNode(E element, Node<E> parent) {
return new AVLNode<>(element,parent);
}

2.6 更新高度

前一步需要计算平衡因子,但是计算平衡因子需要某一节点左右子树的高度差,换句话说就是需要左右子树的高度。在前一步中,我们仅仅是设置了表示高度的成员变量height,当我们给AVL树添加节点后,需要更新其父节点(祖先节点)的高度以便计算平衡因子。

我们可以在AVLNode内部类中编写更新AVL树节点高度的方法updateHeight()

1
2
3
4
5
6
7
8
9
10
private static class AVLNode<E> extends Node<E> {
......

// 更新节点高度
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);
}
}

在前一节中,我们计划编写一个afterAdd(Node<E> node)方法,用于表示添加节点后需要做出的调整,同时该方法传入参数类型为Node,而在AVLTree类中节点类型为AVLNode,我们在afterAdd(Node<E> node)方法中需要对参数 node 进行强制类型转换,为了避免大量的类型转换代码,我们可以将节点高度更新的方法再次进行封装。在AVLTree类中再次封装节点高度更新方法:

1
2
3
4
// 节点高度更新封装
private void updateHeight(Node<E> node){
((AVLNode<E>) node).updateHeight();
}

2.7 旋转准备

接下来就是旋转前的最后准备了,前面已经分析过两大种单旋—— LL 和 RR (双旋由单旋组合而成),而旋转的重点就是要知道到底是哪种失衡,因此我们最后的准备就是判断是哪种情况导致的失衡。

在此之前,我们需要在AVLTree类中重写afterAdd(Node<E> node),在这个方法中编写添加节点后需要做的操作。

关于afterAdd(Node<E> node),有以下几点需要分析:

  • 参数 node 表示新添加的节点
  • 这个方法主要用来更新高度修复平衡
  • 我们可以沿着插入节点的父节点一直向上寻找,查看有无失衡或直到根节点
  • 当前节点处于平衡状态时,我们需要更新它的高度(因为插入了新节点,新节点的祖先节点高度会发生改变)
  • 当前节点处于失衡状态时,我们需要让其恢复平衡,最后跳出循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
protected void afterAdd(Node<E> node) { // node是新添加的节点
// 平衡修复 --> 对最低失衡节点进行修复
// 可能不存在失衡节点 一路向上找即可
while ((node = node.parent) != null) {
if (isBalanced(node)) {// 当node平衡时
// 更新高度
updateHeight(node);
} else {// 当node不平衡时
// 恢复平衡
rebalance(node);
// 整棵树恢复平衡后跳出循环
break;
}
}
}

在上述代码中,更新节点高度的方法updateHeight()已经编写好了,但是恢复平衡的方法rebalance()还没有编写,接下来就是他的主场! ​ 😏

首先我们知道,节点失衡是一系列的,所谓一系列就是指当一个节点发生失衡后,最坏可能其祖先节点都失衡。因此,要想恢复平衡就是要针对高度最低的失衡点进行修复。在上述afterAdd()代码中已经成功获取到了高度最低的失衡点:

获取高度最低失衡点

接下来要做的就是针对这个节点判断失衡方式并进行恢复。判断节点失衡反思是针对失衡点的子节点进行判断的,如果失衡节点的左子节点的左子节点位置插入了节点,并导致AVL树失衡,我们称这种失衡方式为 LL 。同理可得其他三种失衡方式。

还需要判断失衡点的子节点是左子节点还是右子节点,而之所以失衡是因为左右子树高度差大于了 1 ,因此我们判断失衡方式是需要获取当前节点高度最高的子节点,因为高度最高的子节点(或其子节点)就是插入节点的位置。

在获取高度最高的子节点时,需要比较左右子树的高度,比较结果有三种: 大于、小于、等于。大于和小于很好处理,而针对等于的情况可以选择左子节点返回,也可以选择右子节点返回,我们规定返回当前节点的位置。即:如果当前节点是其父节点的左节点,那么就返回左子节点;反之返回右子节点。

针对这种情况,我们在BinaryTree类中的静态嵌套类Node中添加方法,以判断当前节点相对于父节点的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 一个节点就是一个Node
protected static class Node<E> {
......

// 判断当前节点是否是其父节点的左子节点
public boolean isLeftChild() {
return parent != null && this == parent.left;
}

// 判断当前节点是否是其父节点的右子节点
public boolean isRightChild() {
return parent != null && this == parent.right;
}
}

然后在AVLTree类中的静态嵌套类AVLNode中编写获取当前节点高度最高的子节点的方法:

1
2
3
4
5
6
7
8
9
10
11
12
private static class AVLNode<E> extends Node<E> {
......

// 获取当前节点高度最高的子节点
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 isLeftChild() ? left : right;
}
}

最后在AVLTree类中编写恢复平衡的方法rebalance(Node<E> grand)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 恢复平衡
* @param grand 高度最低的不平衡节点
*/
private 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);
}
}
}

上述代码中右旋方法rotateRight(Node<E> node)和左旋方法rotateLeft(Node<E> node)尚未编写,我们在下节进行重点分析

1
2
3
4
5
6
7
8
9
// 左旋转
private void rotateLeft(Node<E> node){
......
}

// 右旋转
private void rotateRight(Node<E> node){
......
}

2.8 实现旋转

2.4 旋转 一节中已经分析了旋转的基本原理,接下来就是要使用代码实现旋转。

代码内容分为两部分:

  • 旋转逻辑
  • 旋转后的处理 (更新父节点、更新高度)

通过比较左旋转和右旋转的旋转逻辑我们可以发现,两种旋转方式除了旋转逻辑之外,旋转后的处理是类似(或者说相同的),为了减少重复代码,我们可以将旋转后的处理操作代码封装成一个方法: afterRotate() ,即:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void afterRotate(Node<E> grand, Node<E> parent, Node<E> child){
// 让 parent 成为根节点
parent.parent = grand.parent;
if (grand.isLeftChild()){
grand.parent.left = parent;
} else if (grand.isRightChild()){
grand.parent.right = parent;
} else { // 没有父节点, grand是根节点
root = parent;
}
// 更新其他节点的父节点
if (child != null){
child.parent = grand;
}
grand.parent = parent;
// 更新高度
updateHeight(grand);
updateHeight(parent);
}

左旋转与右旋转逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 左旋转
private void rotateLeft(Node<E> grand){
Node<E> parent = grand.right;
Node<E> child = parent.left;
// 旋转
grand.right = child;
parent.left = grand;
// 旋转后
afterRotate(grand,parent,child);
}

// 右旋转
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);
}

2.9 旋转统一

旋转统一

在实现旋转时,我们编写了右旋转rotateRight(Node<E> grand)和左旋转rotateLeft(Node<E> grand)对应的方法,但是通过上面的图可以发现旋转是可以使用一个方法来完成的。

a、c、e、g表示子树,可以为空;b、d、f表示节点。它们的高度顺序为: a < b < c < d < e < f < g。 四种情况下最终得到的旋转后的子树都很类似,仅仅是插入节点位置不同。

利用这个原理,可以编写以下用来统一旋转方法rotate()的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
private void rotate(
Node<E> r, // 根节点
Node<E> a, Node<E> b, Node<E> c,
Node<E> d,
Node<E> e, Node<E> f, Node<E> g) {
// 让 d 成为这棵子树的根节点
d.parent = r.parent;
if (r.isLeftChild()) {
r.parent.left = d;
} else if (r.isRightChild()) {
r.parent.right = d;
} else {
root = d;
}

// a - b - c
b.left = a;
if (a != null) {
a.parent = b;
}
b.right = c;
if (c != null) {
c.parent = b;
}
updateHeight(b);

// e - f - g
f.left = e;
if (e != null) {
e.parent = f;
}
f.right = g;
if (g != null) {
g.parent = f;
}
updateHeight(f);

// b - d - f
d.left = b;
d.right = f;
b.parent = d;
f.parent = d;
updateHeight(d);
}

然后改写一下恢复平衡的方法rebalance()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 恢复平衡
*
* @param grand 高度最低的不平衡节点
*/
private void rebalance2(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
rotate(grand, node.left, node, node.right,
parent, parent.right, grand, grand.right);
} else { // LR
rotate(grand, parent.left, parent, node.left,
node, node.right, grand, grand.right);
}
} else { // R
if (node.isLeftChild()) { // RL
rotate(grand, grand.left, grand, node.left,
node, node.right, parent, parent.right);
} else { // RR
rotate(grand, grand.left, grand, parent.left,
parent, node.left, node, node.right);
}
}
}

我们从最开始给出的示意图可以看到,ag在旋转前后是没有改变的,针对AVL树,我们是可以不用处理这两个地方的。但为了泛用,我依旧将它们进行处理。

2.10 删除失衡

删除失衡情况一 删除失衡情况二

原本这棵子树是平衡的,但当我们删除这棵子树中的元素 16 时,可能会导致父节点祖先节点失衡(只有一个节点会失衡), 其他节点都不可能失衡

针对父节点失衡:删除节点后,删除节点的父节点可能失衡,也可能不失衡。

假设节点 15 的左子树为空, 右子节点只有一个 16 ,当我们删除 16 时,虽然节点 15 的高度发生了改变,但是并没有失衡。

针对上述子树不做任何变化,直接删除节点 16 ,那么节点 15 会失衡,且只有节点 15 会失衡。因为删除节点 16 后,节点 15 的平衡因子发生了变化,但是其高度并没有变化,因此不会影响到其他节点。

同时,我们还可以得出一个结论: 如果删除一个节点导致其父节点失衡,那么删除节点一定位于其父节点高度较小的子树中除此之外,节点恢复平衡后,还可能导致更高层祖先节点失衡。

2.11 删除旋转

LL - 右旋转(单旋)

删除右旋转

原本子树是平衡的,当我们删除 红色节点 后,子树处于失衡状态,且是节点 g 处于失衡,因此需要进行恢复平衡。进一步可以得出,失衡的原因是 LL ,因此进行右旋转即可。

旋转后整棵子树恢复平衡了,但是并不代表整棵树恢复了平衡。如果旋转前后子树的高度发生了变化,即: 绿色节点不存在,这棵子树的高度减一。未删除节点前,恰逢这棵子树的父节点的另外一棵子树的高度比这棵子树大 1 ,如果这时候绿色节点不存在,那么导致高度差成为 2 ,这棵子树的父节点失衡。

因此,如果绿色节点不存在,更高层的祖先节点可能也会失衡,需要再次恢复平衡,然后有可能导致更高层的祖先节点失衡…

极端情况下,所有祖先节点都需要进行恢复平衡,共 O(logn)次调整。

RR - 左旋转(单旋)

删除左旋转

LR - RR 左旋转,LL 右旋转(双旋)

遇上 LR 的情况,需要先对 p 进行 RR 左旋转,再对 g 进行 LL 右旋转(双旋)。过程如下:

删除LR双旋

RL - LL 右旋转,RR 左旋转(双旋)

遇上 RL 的情况,需要先对 p 进行 LL 右旋转,再对 g 进行 RR 左旋转(双旋)。过程如下:

删除RL双旋


绿色节点为删除节点后子树高度变化的标志,如果绿色节点不存在,那么整棵子树的高度减一,极有可能导致这棵子树的父节点失衡,然后需要再次恢复平衡,然后又可能导致这棵子树父节点的父节点(祖先节点)失衡,而陷入一个循环…

2.12 实现删除

与添加节点类似,我们同样需要在BST类中添加一个方法afterRemove(Node<E> node),表示删除节点后需要进行的调整。

1
2
3
4
5
6
7
8
9
10
11
/**
* 删除节点之后的调整
* node是被删除的节点
* 或者用以取代被删除节点的子节点(当被删除节点的度为 1 ,红黑树中使用)
* */
// 在其他代码中实现
protected void afterRemove(Node<E> node){ }
// 删除某一元素
public void remove(E element) {
remove(node(element));
}

afterRemove(Node<E> node)BST类中为空,表明该方法在二叉搜索树中的删除方法不用被实现,在需要进行调整的地方进行重写(比如:AVL树中的节点删除操作)。

然后修改BST类中删除节点的方法remove(Node<E> node),在这个方法中合适的位置添加afterRemove()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 删除某一节点
private void remove(Node<E> node) {
if (node == null) return;
size--;
if (node.hasTwoChildren()) { // 度为2的节点
// 找到后继节点
Node<E> s = successor(node);
// 用后继节点的值覆盖被删除节点的值
node.element = s.element;
// 变量node指向其后继节点,等待后续删除
node = s;
}
// 删除node节点(node的度必然为1或0)
Node<E> replacement = node.left != null ? node.left : node.right;
if (replacement != null) { // node度为1
// 更改parent
replacement.parent = node.parent;
// 更改noded的parent的left、right的指向
if (node.parent == null) { // node度为1,且为根节点
root = replacement;
} else if (node == node.parent.left) {
node.parent.left = replacement;
} else { // node == node.parent.right
node.parent.right = replacement;
}

// 被删除的节点
afterRemove(node);
} else if (node.parent == null) { // node度为0,是叶子节点,并且是根节点
root = null;
// 被删除的节点
afterRemove(node);
} else { // node是叶子节点,但不是根节点
if (node == node.parent.left) {
node.parent.left = null;
} else {
node.parent.right = null;
}
// 被删除的节点
afterRemove(node);
}
}

那么什么地方是合适的位置?我们知道在BST中,删除节点有时并不是真的删除(地址)了需要被删除的节点,可能删除的是它的前驱或后继节点,然后还需要进行覆盖。覆盖时会改变节点的左右子树、父节点,afterRemove()方法就可以放在改变后,表示在节点真正被删除后对完整的树进行平衡恢复,而不是一删除就进行恢复。

PS:在AVL树中,我们可以看出afterRemove()方法都是在执行完覆盖调用的,那么可以将它直接写在最后吗?答案是可以的,但是为了后续 红黑树 的实现,不建议这样做,因为红黑树的删除后的调整在不同的位置执行不同。

添加失衡和删除失衡所用的旋转逻辑是一样,代码是一样,可以直接复用。

最后在AVLTree中重写afterRemove()方法:

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void afterRemove(Node<E> node) {
while ((node = node.parent) != null) {
if (isBalanced(node)) {// 当node平衡时
// 更新高度
updateHeight(node);
} else {// 当node不平衡时
// 恢复平衡
rebalance(node);
}
}
}

这样,删除的实现就完成了! 🎉 🎊

Q&A

  • afterRemove()方法与afterAdd()方法仅是一个break的差别,这是为什么?

两者找寻失衡节点、更新高度、恢复平衡的逻辑是一样的,因此两者的打码大致一样。添加失衡只需要修复高度最低的失衡节点就能让整棵树恢复平衡,因此使用break跳出循环;但对于删除节点是不行的,因为节点恢复平衡后,还可能导致更高层的“祖宗”节点失衡,因此需要一直遍历,直到根节点,故不使用break

  • afterRemove()方法中使用node = node.parent进行循环遍历不会出现空指针异常吗?

不会出现。从BST的节点删除方法中可以看到:我们虽然删除了选中的节点,但只是取消了其他节点对被删除节点的应用,而被删除节点对其父节点的引用并没有取消,因此是不会出现空指针异常的。

2.13 AVL树总结

添加

  • 可能会导致所有祖先节点都失衡
  • 只要让高度最低的失衡节点恢复平衡,整棵树就恢复了平衡【仅需O(1)次调整】

删除

  • 只可能会导致父节点祖先节点失衡(只有一个节点会失衡)
  • 节点恢复平衡后,可能会导致更高层的祖先节点失衡【最多需要O(logn)次调整】

平均时间复杂度

  • 搜索: O(logn)
  • 添加: O(logn),仅需O(1)次的旋转操作(并不是说只操作 1 次,而是说旋转次数是常数级别的)
  • 删除: O(logn),最多需要O(logn)次的旋转操作