封面画师:ツチヤ     封面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)次的旋转操作