AVL 树:通过旋转保持平衡

围绕平衡因子、LL/RR/LR/RL 旋转、高度更新和不变量测试理解严格平衡树。

已发布文章计算机基础入门7 分钟阅读发布于 2026年9月1日更新于 2026年9月15日
文章目录
  1. 它从哪里来
  2. 平衡因子
  3. 四种失衡
  4. 插入流程
  5. 左旋与右旋要保持三类信息
  6. 高度更新顺序
  7. AVL 与红黑树
  8. 删除比插入更复杂
  9. 性能与使用场景
  10. 测试清单
知识目录数据结构与算法:从基础到工程实践64 / 77
LOCAL NOTE高亮并记笔记
0 / 1000 · 只保存在这台设备
READER NOTESAVL 树:通过旋转保持平衡》的本机笔记

我曾经把“AVL 树:通过旋转保持平衡”学成了一组互不相干的名词和代码。具体表现是:AVL 的四种旋转我最初完全靠图硬记,隔一段时间就忘,后来从失衡节点和插入方向重新推,才不再依赖口诀。后来我尝试只保留一个问题:它到底在减少哪一种重复工作?顺着这个问题,整块知识才稳定下来。

AVL 是严格自平衡的二叉搜索树。对任意节点,左右子树高度差的绝对值不超过 1。它保留 BST 的有序关系,同时把树高稳定控制在 O(log n)

它从哪里来

AVL 树由 Georgy Adelson-Velsky 与 Evgenii Landis 在 1962 年发表,是最早的自平衡二叉搜索树之一。它用左右子树高度差约束树高,说明搜索树可以在每次更新后做局部旋转,从而保证最坏查询时间。

平衡因子

常见定义为:

balance(node) = height(left) - height(right)

合法值是 -1、0、1。插入或删除后若出现 2-2,就要旋转。节点通常缓存高度,使每次更新只需根据两个孩子重新计算。

四种失衡

类型 新节点方向 修复方式
LL 左孩子的左侧 右旋
RR 右孩子的右侧 左旋
LR 左孩子的右侧 先左旋左孩子,再右旋
RL 右孩子的左侧 先右旋右孩子,再左旋

以右旋为例:

AVL 的 LL、RR、LR、RL 四种失衡与旋转图

图:直线失衡使用单旋,折线失衡使用双旋

a < x < b < y < c 的中序关系在旋转前后不变。旋转不是交换数值,而是局部重连节点,同时保持 BST 顺序。

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

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

插入流程

AVL 插入先按 BST 规则递归找到位置,再沿返回路径更新高度并重平衡:

private Node insert(Node node, int value) {
    if (node == null) return new Node(value);
    if (value < node.value) node.left = insert(node.left, value);
    else if (value > node.value) node.right = insert(node.right, value);
    else return node;
    return rebalance(node);
}

递归返回值必须重新赋给 node.leftnode.right,因为旋转后子树根可能已经变化。最外层同样要写 root = insert(root, value)

左旋与右旋要保持三类信息

  1. BST 的中序顺序。
  2. 父子引用完整,无节点丢失。
  3. 高度从下到上重新计算。

以左旋为例:

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

先保存 middle 是关键。它仍大于 x、小于 y,旋转后应成为 x 的右子树。

高度更新顺序

旋转后应先更新变成下层的旧根,再更新新根。顺序错误会把旧高度继续传播,造成后续误判。实现时最好把“不变量检查”写入测试:递归验证有序性、高度值和平衡因子,而不只是断言查询结果。

private int verify(Node node, long min, long max) {
    if (node == null) return 0;
    if (node.value <= min || node.value >= max) throw new AssertionError("BST order");
    int left = verify(node.left, min, node.value);
    int right = verify(node.right, node.value, max);
    if (Math.abs(left - right) > 1) throw new AssertionError("AVL balance");
    int expected = Math.max(left, right) + 1;
    if (node.height != expected) throw new AssertionError("height cache");
    return expected;
}

验证器必须独立重新计算高度,不能直接相信待测树缓存的 height。

AVL 与红黑树

AVL 更严格平衡,查询密集场景可能拥有更短路径;红黑树允许更松的高度边界,更新时通常旋转更少。Java 的 TreeMap/TreeSet 使用红黑树,不代表 AVL 没有价值,而是通用容器需要在查询、更新和实现成熟度之间平衡。

删除比插入更复杂

插入后通常沿祖先路径找到第一个失衡点即可修复;删除可能让多个祖先高度继续下降,需要一路向上检查。若业务没有特殊需求,不应把教学版 AVL 直接用在生产系统。

删除仍先执行 BST 删除,再对返回路径上的每个节点调用 rebalance。与插入不同,判断双旋方向时不能只看被删除值的位置,应读取孩子自身的平衡因子,因为高度变化可能来自另一侧。

性能与使用场景

AVL 的高度约束比红黑树严格,读多写少、有序查询密集时可能拥有更短路径;更新频繁的通用 Map 往往更偏向红黑树。实际选择还受标准库、内存布局和并发方案影响,不能仅凭旋转次数下结论。

测试清单

  • 分别构造 LL、RR、LR、RL 四种旋转。
  • 递增、递减和随机插入后验证树高。
  • 删除叶子、单孩子、双孩子和根。
  • 每次操作后运行完整不变量验证器。
  • 与 TreeSet 对照外部集合行为。
JARVIS · 当前文章

有哪里没看懂?可以只问这篇。

Jarvis 会限定在《AVL 树:通过旋转保持平衡》及其公开关联内容中检索,并把引用定位回原文章节。

JARVIS / ARTICLE针对《AVL 树:通过旋转保持平衡》提问
当前范围AVL 树:通过旋转保持平衡不会悄悄扩大到全站
0 / 1000

准备好了。当前只会围绕AVL 树:通过旋转保持平衡回答。

READER SIGNAL

这篇内容对你有帮助吗?

不需要登录。你的反馈会直接进入作者待处理列表。