Java中的AVL树指南

2025/04/19

1. 简介

在本教程中,我们将介绍AVL树,并研究插入、删除和搜索值的算法。

2. 什么是AVL树?

AVL树以其发明者Adelson-Velsky和Landis的名字命名,是一种自平衡二叉搜索树(BST)。

自平衡树是一种二叉搜索树,它按照一定的平衡规则在插入和删除后平衡高度

二叉搜索树(BST)的最坏情况时间复杂度取决于树的高度,具体来说,就是从树的根节点到某个节点的最长路径。对于一个有N个节点的二叉搜索树(BST),假设每个节点只有0个或1个子节点。因此,它的高度等于N,最坏情况下的搜索时间为O(N)。因此,我们设计二叉搜索树(BST)的主要目标是使最大高度接近log(N)。

节点N的平衡因子是height(right(N)) – height(left(N)),在AVL树中,节点的平衡因子只能是1、0或-1中的一个值

让我们为树定义一个Node对象:

public class Node {
    int key;
    int height;
    Node left;
    Node right;
    // ...
}

接下来,让我们定义AVLTree:

public class AVLTree {

    private Node root;

    void updateHeight(Node n) {
        n.height = 1 + Math.max(height(n.left), height(n.right));
    }

    int height(Node n) {
        return n == null ? -1 : n.height;
    }

    int getBalance(Node n) {
        return (n == null) ? 0 : height(n.right) - height(n.left);
    }

    // ...
}

3. 如何平衡AVL树?

AVL树在插入或删除节点后会检查其节点的平衡因子,如果节点的平衡因子大于1或小于-1,则树会重新平衡自身。

重新平衡树有两种操作:

  • 右旋
  • 左旋

3.1 右旋

让我们从右旋开始。

假设我们有一个名为T1的BST,其中Y为根节点,X为Y的左孩子,Z为X的右孩子。给定BST的特性,我们知道X < Z < Y。

在对Y进行右旋转之后,我们得到一棵树,称为T2,其中X为根,Y为X的右孩子,Z为Y的左孩子。T2仍然是BST,因为它保持了X < Z < Y的顺序。

让我们看一下AVLTree的右旋操作:

Node rotateRight(Node y) {
    Node x = y.left;
    Node z = x.right;
    x.right = y;
    y.left = z;
    updateHeight(y);
    updateHeight(x);
    return x;
}

3.2 左旋

假设有一个名为T1的BST,其中Y为根节点,X为Y的右孩子,Z为X的左孩子。由此,我们知道Y < Z < X。

Y左旋转后,我们得到一棵树T2,其中X为根,Y为X的左孩子,Z为Y的右孩子。T2仍然是BST,因为它保持了Y < Z < X的顺序。

让我们看一下AVLTree的左旋操作:

Node rotateLeft(Node y) {
    Node x = y.right;
    Node z = x.left;
    x.left = y;
    y.right = z;
    updateHeight(y);
    updateHeight(x);
    return x;
}

3.3 重平衡技术

我们可以将右旋和左旋操作以更复杂的组合使用,以使AVL树在其节点发生任何更改后仍保持平衡。在不平衡的结构中,至少有一个节点的平衡因子等于2或-2,让我们看看如何在这些情况下平衡树。

当节点Z的平衡因子为2时,以Z为根的子树处于这两种状态之一,将Y视为Z的右孩子。

对于第一种情况,Y的右孩子(X)的高度大于左孩子(T2)的高度,我们可以通过Z的左旋轻松地重新平衡树。

对于第二种情况,Y的右孩子(T4)的高度小于左孩子(X)的高度,这种情况需要组合使用旋转操作。

在这种情况下,我们首先将Y轴向右旋转,使树的形状与上一种情况相同。然后,我们可以通过Z向左旋转来重新平衡树。

另外,当节点Z的平衡因子为-2时,其子树处于这两种状态之一,因此我们将Z视为根,将Y视为其左孩子。

Y的左孩子的高度大于其右孩子的高度,因此我们通过Z的右旋转来平衡树。

或者在第二种情况下,Y的右孩子的高度大于其左孩子的高度。

因此,首先,我们通过Y左旋转将其转换为以前的形状,然后通过Z右旋转使树保持平衡。

让我们看一下AVLTree的重新平衡操作:

Node rebalance(Node z) {
    updateHeight(z);
    int balance = getBalance(z);
    if (balance > 1) {
        if (height(z.right.right) > height(z.right.left)) {
            z = rotateLeft(z);
        } else {
            z.right = rotateRight(z.right);
            z = rotateLeft(z);
        }
    } else if (balance < -1) {
        if (height(z.left.left) > height(z.left.right))
            z = rotateRight(z);
        else {
            z.left = rotateLeft(z.left);
            z = rotateRight(z);
        }
    }
    return z;
}

在插入或删除一个节点后,我们将对从更改的节点到根的路径中的所有节点使用重平衡

4. 插入节点

当我们要在树中插入一个键时,我们必须找到它的正确位置以符合二叉搜索树(BST)规则。因此,我们从根节点开始,将其值与新键进行比较。如果键值更大,则继续向右移动;否则,则从左孩子节点移动。

一旦我们找到合适的父节点,我们就会根据值将新键作为节点添加到左侧或右侧。

插入节点后,我们得到了一棵BST,但它可能不是AVL树。因此,我们检查平衡因子,并针对从新节点到根路径上的所有节点重新平衡BST。

我们来看一下插入操作:

Node insert(Node root, int key) {
    if (root == null) {
        return new Node(key);
    } else if (root.key > key) {
        root.left = insert(root.left, key);
    } else if (root.key < key) {
        root.right = insert(root.right, key);
    } else {
        throw new RuntimeException("duplicate Key!");
    }
    return rebalance(root);
}

重要的是要记住,键在树中是唯一的-没有两个节点共享相同的键

插入算法的时间复杂度是高度的函数,由于我们的树是平衡的,我们可以假设最坏情况下的时间复杂度为O(log(N))。

5. 删除节点

要从树中删除一个键,我们首先必须在BST中找到它。

找到节点(称为Z)后,我们必须引入新的候选节点来替代它在树中的位置。如果Z是叶子节点,则候选节点为空。如果Z只有一个子节点,则该子节点就是候选节点;但如果Z有两个子节点,则过程会稍微复杂一些。

假设Z的右孩子名为Y,首先,我们找到Y的最左边的节点并将其称为X。然后,我们将Z的新值设置为等于X的值,并继续从Y中删除X。

最后,我们在最后调用重平衡方法来保持BST为AVL树。

这是我们的删除方法:

Node delete(Node node, int key) {
    if (node == null) {
        return node;
    } else if (node.key > key) {
        node.left = delete(node.left, key);
    } else if (node.key < key) {
        node.right = delete(node.right, key);
    } else {
        if (node.left == null || node.right == null) {
            node = (node.left == null) ? node.right : node.left;
        } else {
            Node mostLeftChild = mostLeftChild(node.right);
            node.key = mostLeftChild.key;
            node.right = delete(node.right, node.key);
        }
    }
    if (node != null) {
        node = rebalance(node);
    }
    return node;
}

删除算法的时间复杂度是树的高度的函数,与插入方法类似,我们可以假设最坏情况下的时间复杂度为O(log(N))。

6. 搜索节点

在AVL树中搜索节点与任何BST中搜索节点相同。

从树的根节点开始,将键与节点的值进行比较。如果键等于值,则返回该节点。如果键大于值,则从右子节点开始搜索,否则继续从左子节点开始搜索。

搜索的时间复杂度是高度的函数,我们可以假设最坏情况下的时间复杂度为O(log(N))。

我们来看示例代码:

Node find(int key) {
    Node current = root;
    while (current != null) {
        if (current.key == key) {
            break;
        }
        current = current.key < key ? current.right : current.left;
    }
    return current;
}

7. 总结

在本教程中,我们实现了具有插入、删除和搜索操作的AVL树。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章