前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >数据结构之AVL树

数据结构之AVL树

作者头像
端碗吹水
发布2021-02-02 10:28:43
4820
发布2021-02-02 10:28:43
举报
文章被收录于专栏:程序猿的大杂烩

平衡树和AVL

我们先来回忆一下二分搜索树所存在的一个问题:当我们按顺序往二分搜索树添加元素时,那么二分搜索树可能就会退化成链表。例如,现在有这样一颗二分搜索树:

接下来我们依次插入如下五个节点:7、6、5、4、3。按照二分搜索树的特性,这棵树就会变成如下这样:

可见在极端的情况下,如果往一棵二分搜索树添加元素时,完全是按照顺序添加的,那么此时二分搜索树就会退化成链表,O(logn) 时间复杂度退化到 O(n)

这是因为二分搜索树不具有自平衡的特性,为了让二分搜索树不退化成链表,我们就得设计一种机制,即便是在按顺序添加元素时,也能让二分搜索树维持平衡。而具有自平衡特性的二叉树或 m 叉树,就称之为平衡树。

而这个“平衡”其实有几种情况,有绝对平衡:任意节点的左右子树高度相等(2-3树);高度平衡:任意节点的左右子树高度相差不超过 1(AVL树);近似平衡:任意节点的左右子树高度相差不超过 2,或者说从根节点到叶子节点的最长路径不大于最短路径的 2 倍(红黑树)。

基本上只要一棵树的高度和节点数量之间的关系始终是 O(logn) ,也就是不会发生退化情况的,就能称之为平衡树。如果敢说一棵树是”平衡“的,就意味着它的高度是 logn 级别的。也就意味着对这棵树的基本操作(增删改查)是 logn 级别的。

其中 AVL 树是最早被发明出来的平衡树,AVL 这个名称来自于它的两位发明者 G.M. Adelson-VelskyE.M. Landis 的首字母,AVL 树在他们1962年的论文中首次提出。所以,可以认为 AVL 树是最早的自平衡二分搜索树结构。AVL 树遵循的是高度平衡,任意节点的左右子树高度相差不超过 1。


计算节点的高度和平衡因子

经过以上的介绍,现在我们已经知道了AVL树是一种平衡的二分搜索树。那么为了维持AVL树的平衡,我们就得做一些额外的工作。首先,我们得知道AVL树的平衡状态,可以通过一些依据判断AVL树是否已经失衡了。如果处于失衡状态,就需要对AVL树做出一系列的调整使得它维持平衡。

判断AVL树是否平衡的主要依据是节点的平衡因子,而平衡因子则通过节点的高度计算得出。下图中,用黑色字体标记的是节点的高度,蓝色字体标记的是节点的平衡因子:

上图中的二叉树不是一棵合格的AVL树,因为只有当一棵二叉树所有节点的平衡因子都是 -1、0、1这 三个值时,这棵二叉树才能算是一棵合格的AVL树。如下图所示:

  • 其中节点 4 的左子树高度是 1,右子树不存在,所以该节点的平衡因子是 1-0=1
  • 节点7的左子树不存在,右子树高度是1,所以平衡因子是 0-1=-1
  • 所有的叶子节点,不存在左右子树,所以平衡因子都是 0

为了计算节点的平衡因子,我们需要在每个节点中新增加一个字段,存储节点的高度。而平衡因子的计算也很简单,用左子节点的高度减去右子节点的高度就可以了。也就是说,平衡因子就是左右子树高度的差值。

接下来,我们先实现AVL树的基础代码:

代码语言:javascript
复制
package tree.avl;

import java.util.ArrayList;

/**
 * AVL树
 *
 * @author 01
 * @date 2021-01-29
 **/
public class AVLTree<K extends Comparable<K>, V> {

    private class Node {
        public K key;
        public V value;
        public Node left, right;
        // 标识节点的高度
        public int height;

        public Node(K key, V value) {
            this.key = key;
            this.value = value;
            left = null;
            right = null;
            // 新节点的默认高度
            height = 1;
        }
    }

    private Node root;
    private int size;

    public AVLTree() {
        root = null;
        size = 0;
    }

    public int getSize() {
        return size;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 获得节点node的高度
     */
    private int getHeight(Node node) {
        return node == null ? 0 : node.height;
    }

    /**
     * 获得节点node的平衡因子
     */
    private int getBalanceFactor(Node node) {
        if (node == null) {
            return 0;
        }

        return getHeight(node.left) - getHeight(node.right);
    }
}

检查二分搜索树性质和平衡性

有了判断平衡状态的依据后,我们就可以判断AVL树的平衡性了。除此之外,由于AVL树本质上是一棵平衡版的二分搜索树,所以我们还需要检查AVL树的二分搜索树性质。因为,调整AVL树的过程中可能会破坏二分搜索树的性质,此时就需要将其“矫正”过来。

判断AVL树的平衡性很简单,就是看各个节点的平衡因子是否大于1即可。因为平衡因子本质上只是左右子树高度的差值,而AVL树的定义是这个差值不能大于1。检查二分搜索树的性质也不难,通过中序遍历就可以做到。因为一棵树满足二分搜索树的性质,那么中序遍历必然是有序的,如果得到的结果是无序的就证明不满足二分搜索树的性质。

具体的实现代码如下:

代码语言:javascript
复制
/**
 * 检查当前的AVL树是否满足二分搜索树的性质
 */
public boolean isBST() {
    ArrayList<K> keys = new ArrayList<>();
    inOrder(root, keys);
    for (int i = 1; i < keys.size(); i++) {
        // 中序遍历一棵二分搜索树所得到的key理应是有序的
        // 如果是无序的,就证明不满足二分搜索树的性质
        if (keys.get(i - 1).compareTo(keys.get(i)) > 0) {
            return false;
        }
    }

    return true;
}

/**
 * 中序遍历以node为根的二叉树,并将每个节点的key放到keys中
 */
private void inOrder(Node node, ArrayList<K> keys) {
    if (node == null) {
        return;
    }

    inOrder(node.left, keys);
    keys.add(node.key);
    inOrder(node.right, keys);
}

/**
 * 检查当前AVL树的平衡性
 */
public boolean isBalanced() {
    return isBalanced(root);
}

/**
 * 判断以Node为根的二叉树是否是一棵平衡二叉树,递归实现
 */
private boolean isBalanced(Node node) {
    if (node == null) {
        return true;
    }

    int balanceFactor = getBalanceFactor(node);
    // AVL对平衡的定义是:左右子树高度相差不能大于1
    if (Math.abs(balanceFactor) > 1) {
        return false;
    }

    return isBalanced(node.left) && isBalanced(node.right);
}

旋转操作的基本原理

经过前面的铺垫,现在我们已经完成了AVL树维持平衡时所需的辅助功能。接下来,我们看看AVL树是怎么维持平衡的。首先,我们得知道AVL树什么时候会发生平衡性被打破的情况。

与其他树形结构一样,当AVL树添加或删除节点时,其平衡性就有可能会被打破。如下图所示:

那么AVL树是怎么维持平衡的呢?之前在红黑树的文章中提到过,红黑树是通过变色、左旋及右旋转这三种操作来维持平衡的。

因为AVL树中的节点没有颜色的概念,所以不存在变色的问题,只有左旋转、右旋转这两种维持平衡的操作。并且AVL树中的左旋转和右旋转,和之前红黑树中所介绍的是一样的。

左旋转:逆时针旋转红黑树的两个节点,使得父节点被自己的右子节点取代,而自己成为自己的左子节点。如下图:

  • 在上图中,身为右子节点的Y取代了X的位置,而X变成了自己的左子节点,因此为左旋转

右旋转:顺时针旋转红黑树的两个节点,使得父节点被自己的左子节点取代,而自己成为自己的右子节点。如下图:

  • 在上图中,身为左子节点的Y取代了X的位置,而X变成了自己的右子节点,因此为右旋转

那么AVL树什么时候需要进行左旋转,什么时候需要进行右旋转呢?这得看树的倾斜情况,因为不同的倾斜情况,需要采取不同的旋转方式。主要分为四种情况,对应着四种旋转方式。这里将其称为:

  • 左左情况(LL),单次右旋转
  • 右右情况(RR),单次左旋转
  • 左右情况(LR),先左旋转,后右旋转
  • 右左情况(RL),先右旋转,后左旋转

如果你有学习过如何还原魔方的话,就会发现AVL树的平衡过程跟魔方的还原非常相似。魔方的还原是有固定公式的:根据色块在一个面上的不同排列情况,都有相应的旋转步骤。只要跟着这个还原步骤,最终就能将魔方还原。

而AVL树的平衡大致过程就是:遇到什么样的节点排布,我们就对应怎么去旋转调整。只要按照这些固定的旋转规则来操作,就能将一个非平衡的AVL树调整成平衡的。这里不同的节点排布就对应着上述所说的四种情况,接下来我们就看看这四种情况及其解法。

1、左左情况(LL),简单来说就是整体左倾的情况,倾斜发生在节点左子树中的最左子节点。如下图:

  • 图中的三角形表示各个节点的子树

在这种情况下,我们需要从下往上找到发生倾斜的子树的根节点,即该子树中平衡因子大于 1 的那个节点。在此例中就是 y 节点,此时我们以 y 节点为轴,进行一次右旋转,从而矫正这棵树:

2、右右情况(RR)是整体右倾的情况,倾斜发生在节点右子树中的最右子节点。如下图:

在这种情况下,同样从下往上找到相应的根节点,然后以根节点 y 为轴,进行一次左旋转:

3、左右情况(LR),倾斜发生在节点左子树中的最右子节点。如下图:

在这种情况下,我们就需要分两步走了,先以 x 节点为轴,进行左旋转:

可以看到此时就转换成了左左情况(LL),那么就只需要按照左左情况的方式,以 y 节点为轴,进行右旋转即可:

4、右左情况(RL),倾斜发生在节点右子树中的最左子节点。如下图:

同样,在这种情况下,我们也需要分两步走,先以 x 节点为轴,进行右旋转:

转换成了右右情况(RR)后,按照这种情况的方式,以 y 节点为轴,进行左旋转:

以上就是AVL树需要调整平衡的四种情况,以及四种对应的调整方式。现在让我们来看本小节最开始的那个例子,在该例子中,以节点4 为根的左子树出现了不平衡的情况。现在来看,该子树正好符合 “左左情况”。于是,我们以节点 4 为轴,进行右旋操作,就让AVL树重新恢复了高度平衡:


左旋转和右旋转的实现

在上一小节中,我们介绍了AVL树为了维持平衡所使用的旋转操作,以及不同情况所对应的不同旋转方式。在本小节中,就让我们用代码来实现AVL树的左旋转和右旋转操作。代码如下:

代码语言:javascript
复制
// 对节点y进行向右旋转操作,返回旋转后新的根节点x
//        y                              x
//       / \                           /   \
//      x   T4     向右旋转 (y)        z     y
//     / \       - - - - - - - ->    / \   / \
//    z   T3                       T1  T2 T3 T4
//   / \
// T1   T2
private Node rightRotate(Node y) {
    Node x = y.left;
    Node T3 = x.right;

    // 向右旋转过程
    x.right = y;
    y.left = T3;

    // 更新height
    y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
    x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;

    return x;
}

// 对节点y进行向左旋转操作,返回旋转后新的根节点x
//    y                             x
//  /  \                          /   \
// T1   x      向左旋转 (y)       y     z
//     / \   - - - - - - - ->   / \   / \
//   T2  z                     T1 T2 T3 T4
//      / \
//     T3 T4
private Node leftRotate(Node y) {
    Node x = y.right;
    Node T2 = x.left;

    // 向左旋转过程
    x.left = y;
    y.right = T2;

    // 更新height
    y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
    x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;

    return x;
}

向AVL树中添加元素

到目前为止,我们就已经了解了AVL树中维持平衡所需的内容。在理论和代码上我们都学习到了如何维持一棵AVL树的平衡性,也已经实现了相应的辅助功能。

那么也就知道在添加和删除元素时,如何解决可能破坏AVL树平衡性的问题。所以,接下来我们就实现向AVL树中添加元素的功能。具体代码如下:

代码语言:javascript
复制
/**
 * 向AVL树中添加新的元素(key, value)
 */
public void add(K key, V value) {
    root = add(root, key, value);
}

/**
 * 向以node为根的AVL中插入元素(key, value),递归实现
 * 返回插入新节点后AVL的根
 */
private Node add(Node node, K key, V value) {
    if (node == null) {
        size++;
        return new Node(key, value);
    }

    if (key.compareTo(node.key) < 0) {
        node.left = add(node.left, key, value);
    } else if (key.compareTo(node.key) > 0) {
        node.right = add(node.right, key, value);
    } else {
        node.value = value;
    }

    // 更新height
    node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right));

    // 计算平衡因子
    int balanceFactor = getBalanceFactor(node);

    // --- 维护平衡 start ---

    // LL
    if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0) {
        return rightRotate(node);
    }

    // RR
    if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0) {
        return leftRotate(node);
    }

    // LR
    if (balanceFactor > 1 && getBalanceFactor(node.left) < 0) {
        node.left = leftRotate(node.left);
        return rightRotate(node);
    }

    // RL
    if (balanceFactor < -1 && getBalanceFactor(node.right) > 0) {
        node.right = rightRotate(node.right);
        return leftRotate(node);
    }

    // --- 维护平衡 end ---

    return node;
}

从AVL树中删除元素

从AVL树中删除元素也会打破AVL树的平衡性,那么在删除元素时如何维持AVL树的平衡呢?如果在删除元素时,打破了AVL树的平衡,其维持平衡的调整方式与之前提到的一样,还是根据那四种情况进行四种旋转操作即可。

因此,有了前面的基础,并且对二分搜索树的删除操作有一定的了解的话,那么对AVL树的删除操作理解起来就比较容易了。无非就是在二分搜索树的删除操作的基础上增加了维护平衡的操作,而这个操作与添加元素时是完全一样的。

我们来看个例子:

如上图所示,我们在AVL树中删除了节点 1,导致父节点 2 的平衡因子变为了 -2,打破了AVL树的平衡。此时,以节点 2 为根的子树正好形成了“右左情况(RL)”,于是我们首先以节点 4 为轴进行右旋转:

然后再以节点 2 为轴进行左旋转:

经过如上步骤后,最终AVL树重新恢复了高度平衡。

AVL树删除操作的具体实现代码如下:

代码语言:javascript
复制
/**
 * 返回以node为根的AVL的最小值所在的节点
 */
private Node minimum(Node node) {
    if (node.left == null) {
        return node;
    }

    return minimum(node.left);
}

/**
 * 从AVL中删除键为key的节点
 */
public V remove(K key) {
    Node node = getNode(root, key);
    if (node != null) {
        root = remove(root, key);
        return node.value;
    }

    return null;
}

/**
 * 删除以node为根的AVL中键为key的节点,递归实现
 * 返回删除节点后新的AVL的根
 */
private Node remove(Node node, K key) {
    if (node == null) {
        return null;
    }

    // 存放被删除的节点
    Node retNode;
    if (key.compareTo(node.key) < 0) {
        // 待删除节点在左子树中
        node.left = remove(node.left, key);
        retNode = node;
    } else if (key.compareTo(node.key) > 0) {
        // 待删除节点在右子树中
        node.right = remove(node.right, key);
        retNode = node;
    } else {
        // 待删除节点左子树为空的情况
        if (node.left == null) {
            Node rightNode = node.right;
            node.right = null;
            size--;
            retNode = rightNode;
        }

        // 待删除节点右子树为空的情况
        else if (node.right == null) {
            Node leftNode = node.left;
            node.left = null;
            size--;
            // return leftNode;
            retNode = leftNode;
        }

        // 待删除节点左右子树均不为空的情况
        else {
            // 找到比待删除节点大的最小节点, 即待删除节点右子树的最小节点
            // 用这个节点顶替待删除节点的位置
            Node successor = minimum(node.right);
            successor.right = remove(node.right, successor.key);
            successor.left = node.left;
            node.left = node.right = null;
            retNode = successor;
        }
    }

    if (retNode == null) {
        return null;
    }

    // 更新height
    retNode.height = 1 + Math.max(getHeight(retNode.left), getHeight(retNode.right));

    // 计算平衡因子
    int balanceFactor = getBalanceFactor(retNode);

    // --- 维护平衡 start ---

    // LL
    if (balanceFactor > 1 && getBalanceFactor(retNode.left) >= 0) {
        return rightRotate(retNode);
    }

    // RR
    if (balanceFactor < -1 && getBalanceFactor(retNode.right) <= 0) {
        return leftRotate(retNode);
    }

    // LR
    if (balanceFactor > 1 && getBalanceFactor(retNode.left) < 0) {
        retNode.left = leftRotate(retNode.left);
        return rightRotate(retNode);
    }

    // RL
    if (balanceFactor < -1 && getBalanceFactor(retNode.right) > 0) {
        retNode.right = rightRotate(retNode.right);
        return leftRotate(retNode);
    }

    // --- 维护平衡 end ---

    return retNode;
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021/01/29 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 平衡树和AVL
  • 计算节点的高度和平衡因子
  • 检查二分搜索树性质和平衡性
  • 旋转操作的基本原理
  • 左旋转和右旋转的实现
  • 向AVL树中添加元素
  • 从AVL树中删除元素
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档