跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

AVL 树核心详解:从概念到 C++ 手撕实现

AVL 树通过维护节点平衡因子(左右子树高度差绝对值不超过 1)解决二叉搜索树退化为链表的问题。深入解析 AVL 树的核心原理,涵盖节点结构定义、插入时的平衡因子更新逻辑、四种旋转操作(LL、RR、LR、RL)的具体实现细节以及删除操作的注意事项。同时提供了验证代码,强调通过计算实际高度而非仅依赖平衡因子来校验树的合法性。适合希望掌握平衡搜索树底层机制的开发者阅读。

霸天发布于 2026/3/15更新于 2026/10/2593 浏览
AVL 树核心详解:从概念到 C++ 手撕实现

AVL 树核心详解

你是否曾在写算法时,用二叉搜索树(BST)存数据却遭遇'超时'暴击?明明理论复杂度是 O(logn),偏偏插入一串有序数据后,树直接退化成链表,查询效率暴跌到 O(n)?这背后的元凶,正是 BST 无法自我平衡的结构性缺陷。而 AVL 树,就是为破解这个痛点而生的平衡大师。

它不仅是首个严格平衡的二叉搜索树,每一个设计细节都藏着精妙的逻辑:为什么平衡因子要选高度差≤1 而非高度相等?旋转操作看似复杂,实则是局部修复全局平衡的贪心策略。这篇内容我们将挖透 AVL 树的底层逻辑,从诞生的根源到旋转与平衡因子的本质,最终让你明白——AVL 树不仅是一种数据结构,更是一套用最小代价解决失衡问题的方法论。

AVL 树的概念

AVL 树是二叉平衡搜索树的一种(红黑树也是)。跟普通二叉搜索树的区别在于,极端场景下 BST 会退化成类似链表的结构,而 AVL 树则不会。

一棵 AVL 树或者是空树,或者是具有以下性质的二叉搜索树:

  1. 它的左右子树都是 AVL 树。
  2. 左右子树高度之差(简称平衡因子)的绝对值不超过 1(即 -1、0 或 1)。

每个节点都有它的平衡因子。AVL 树增删查改的时间复杂度都是 O(logn),其中 n 是元素个数。

AVL 树的模拟实现

虽然 AVL 树和红黑树在手撕代码中考察频率不如基础结构高,但理解其整体模拟实现的思想以及旋转的手撕过程非常关键。这里我们重点看 C++ 的实现细节。

节点结构与插入逻辑

首先定义节点,需要存储键值对、左右孩子指针、父指针以及平衡因子 _bf。

template<class K, class V>
struct AVLTreeNode {
    pair<K, V> _kv;
    AVLTreeNode<K, V>* _left;
    AVLTreeNode<K, V>* _right;
    AVLTreeNode<K, V>* _parent;
    int _bf; // 平衡因子 balance factor

    AVLTreeNode(const pair<K, V>& kv)
        :_kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _bf(0) {}
};

插入时的核心难点在于更新平衡因子和必要的旋转。插入完成后,我们需要自底向上更新祖先节点的平衡因子。

template<class K, class V>
class AVLTree {
    typedef AVLTreeNode<K, V> Node;
public:
    bool Insert(const pair<K, V>& kv) {
        if (_root == nullptr) {
            _root = new Node(kv);
            return true;
        }

        Node* parent = nullptr;
        Node* cur = _root;
        while (cur) {
            if (cur->_kv.first < kv.first) {
                parent = cur;
                cur = cur->_right;
            } else if (cur->_kv.first > kv.first) {
                parent = cur;
                cur = cur->_left;
            } else {
                return false; // 重复元素不插入
            }
        }

        cur = new Node(kv);
        if (parent->_kv.first < kv.first) {
            parent->_right = cur;
        } else {
            parent->_left = cur;
        }
        cur->_parent = parent;

        // 更新平衡因子
        while (parent) {
            if (cur == parent->_left) {
                parent->_bf--;
            } else {
                parent->_bf++;
            }

            if (parent->_bf == 0) {
                // 更新结束,说明该子树高度未变,无需继续向上
                break;
            } else if (parent->_bf == 1 || parent->_bf == -1) {
                // 继续往上更新,因为高度变了
                cur = parent;
                parent = parent->_parent;
            } else if (parent->_bf == 2 || parent->_bf == -2) {
                // 子树不平衡了,需要旋转
                if (parent->_bf == 2 && cur->_bf == 1) {
                    RotateL(parent); // LL 型
                } else if (parent->_bf == -2 && cur->_bf == -1) {
                    RotateR(parent); // RR 型
                } else if (parent->_bf == 2 && cur->_bf == -1) {
                    RotateRL(parent); // RL 型
                } else if (parent->_bf == -2 && cur->_bf == 1) {
                    RotateLR(parent); // LR 型
                }
                break;
            } else {
                assert(false); // 防止逻辑错误
            }
        }
        return true;
    }

关于平衡因子的更新

在插入过程中,平衡因子的更新遵循以下规律:

  1. 新增节点在左,parent 平衡因子减减。
  2. 新增节点在右,parent 平衡因子加加。
  3. 更新后 parent 平衡因子等于 0,祖先的平衡因子不用动了,更新结束。
  4. 更新后 parent 平衡因子等于 1 或 -1,祖先平衡因子要动,依旧按前两点规则。
  5. 更新后 parent 平衡因子等于 2 或 -2,要对 parent 所在子树进行旋转,之后就更新结束了。
  6. 如果更新到根节点还没结束,也停止。

旋转操作详解

旋转时要确保始终是搜索树。AVL 树的旋转分为四种情况,判断依据是新节点插入的位置相对于不平衡点形成的路径形状。

1. 左旋 (RotateL) 新节点插入较高右子树的右侧(RR 型)。把 cur 的左节点接到 parent 的右边,然后把 parent 接到 cur 的左边。

void RotateL(Node* parent) {
    Node* cur = parent->_right;
    Node* curleft = cur->_left;
    
    parent->_right = curleft;
    if (curleft) curleft->_parent = parent;
    
    cur->_left = parent;
    
    Node* ppnode = parent->_parent;
    parent->_parent = cur;
    
    if (parent == _root) {
        _root = cur;
        cur->_parent = nullptr;
    } else {
        if (ppnode->_left == parent) {
            ppnode->_left = cur;
        } else {
            ppnode->_right = cur;
        }
        cur->_parent = ppnode;
    }
    
    // 单旋后,平衡因子通常归零
    parent->_bf = cur->_bf = 0;
}

2. 右旋 (RotateR) 新节点插入较高左子树的左侧(LL 型)。把 cur 的右节点接到 parent 的左边,然后把 parent 接到 cur 的右边。

void RotateR(Node* parent) {
    Node* cur = parent->_left;
    Node* curright = cur->_right;
    
    parent->_left = curright;
    if (curright) curright->_parent = parent;
    
    Node* ppnode = parent->_parent;
    cur->_right = parent;
    parent->_parent = cur;
    
    if (ppnode == nullptr) {
        _root = cur;
        cur->_parent = nullptr;
    } else {
        if (ppnode->_left == parent) {
            ppnode->_left = cur;
        } else {
            ppnode->_right = cur;
        }
        cur->_parent = ppnode;
    }
    
    parent->_bf = cur->_bf = 0;
}

3. 双旋 (RotateLR & RotateRL) 当插入位置形成折线时需要双旋。例如新节点插入较高左子树的右侧(LR 型),需要先左旋再右旋。

// 先左旋再右旋
void RotateLR(Node* parent) {
    Node* cur = parent->_left;
    Node* curright = cur->_right;
    int bf = curright->_bf;
    
    RotateL(parent->_left);
    RotateR(parent);
    
    // 双旋后平衡因子需根据插入位置调整
    if (bf == 0) {
        parent->_bf = 0; cur->_bf = 0; curright->_bf = 0;
    } else if (bf == -1) {
        parent->_bf = 1; cur->_bf = 0; curright->_bf = 0;
    } else if (bf == 1) {
        parent->_bf = 0; cur->_bf = -1; curright->_bf = 0;
    }
}

// 先右旋再左旋
void RotateRL(Node* parent) {
    Node* cur = parent->_right;
    Node* curleft = cur->_left;
    int bf = curleft->_bf;
    
    RotateR(parent->_right);
    RotateL(parent);
    
    if (bf == 0) {
        cur->_bf = 0; curleft->_bf = 0; parent->_bf = 0;
    } else if (bf == 1) {
        cur->_bf = 0; curleft->_bf = 0; parent->_bf = -1;
    } else if (bf == -1) {
        cur->_bf = 1; curleft->_bf = 0; parent->_bf = 0;
    }
}

快速判断旋转类型:

  • 沿着 cur 点向右偏的直线 -> 右旋
  • 沿着 cur 点向左偏的直线 -> 左旋
  • cur 在最左边的折线 -> 先左旋再右旋
  • cur 在最右边的折线 -> 先右旋再左旋

AVL 树的删除

删除操作的步骤与插入类似,但规则有所不同:

  1. 按搜索树的规则查找并删除节点。
  2. 更新平衡因子。
  3. 出现异常(平衡因子变为 ±2)时进行旋转。

注意: 这里的规则跟插入的时候不一样。删除时,如果节点的平衡因子更新为 ±1 就停止(因为高度没变),为 0 的话才向上走(因为高度降低了)。异常之后旋转的条件依然是平衡因子为 ±2 时处理。

AVL 树的验证

如何验证你写的 AVL 树是否正确?不能仅依赖平衡因子,因为可能更新平衡因子的逻辑本身就有 BUG。也不能仅靠中序遍历有序来证明,那只能说明它是二叉搜索树。

最稳妥的方式是计算实际高度并与平衡因子对比。

int Height(Node* root) {
    if (root == nullptr) return 0;
    int leftHeight = Height(root->_left);
    int rightHeight = Height(root->_right);
    return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

bool IsBalance(Node* root) {
    if (root == nullptr) return true;
    int leftHight = Height(root->_left);
    int rightHight = Height(root->_right);
    
    if (abs(rightHight - leftHight) != abs(root->_bf)) {
        cout << "平衡因子异常:" << root->_kv.first << endl;
        return false;
    }
    
    return abs(rightHight - leftHight) < 2 && 
           IsBalance(root->_left) && 
           IsBalance(root->_right);
}

思考与练习

现有一棵无重复关键字的平衡二叉树(AVL 树),对其进行中序遍历可得到一个降序序列。下列关于该平衡二叉树的叙述中,正确的是:

A. 根结点的度一定为 2 B. 树中最小元素一定是叶结点 C. 最后插入的元素一定是叶结点 D. 树中最大元素一定是无左子树

解析: 选 D。因为是降序序列,说明是反序的中序遍历(右->根->左),或者理解为树的结构使得最大值在左侧路径的最末端。对于 AVL 树,最大元素一定没有右子树(如果是升序),这里是降序,意味着最大元素在左子树的最深处且无左子树。具体取决于中序遍历的定义方向,但通常最大元素在无左子树的情况下更容易被访问到(视遍历顺序而定)。此题主要考察对树结构的理解,C 选项错在插入后可能会旋转导致原叶子变成非叶子。

目录

  1. AVL 树核心详解
  2. AVL 树的概念
  3. AVL 树的模拟实现
  4. 节点结构与插入逻辑
  5. 关于平衡因子的更新
  6. 旋转操作详解
  7. AVL 树的删除
  8. AVL 树的验证
  9. 思考与练习

更多推荐文章

查看全部
  • 现代 C++ 新特性 constexpr:从 C++11 到 C++20 的演进
  • Python 异步编程与协程实战指南
  • Linux Shell 命令与语法底层执行逻辑
  • Python 生成 12 位随机密码示例
  • ComfyUI v0.18.0 发布:显存内存优化、VAE 架构进化、API 节点与前端升级
  • GitHub 学生认证与 PyCharm Copilot 配置全流程指南
  • CMake 核心概念与实战:目标、属性、API 及静态库构建
  • 即答侠 InterviewAssistant 技术解析:AI 面试辅助与简历优化实战
  • 高空长航时无人机热管理系统中抗辐照 MCU 的可靠性研究
  • 统信 UOS V2500 服务器 OpenClaw AI Agent 安装部署指南
  • Unity VR 高分辨率全景视频播放性能优化
  • Git 原理与使用
  • FPGA 开发环境搭建:Quartus II 13.1 与 ModelSim 安装配置指南
  • ERNIE-4.5-0.3B 轻量模型部署指南与实战测评
  • OpenHarmony与华为云IoT智能家居开发指南
  • 使用 AI 视觉编程模型优化 Unity 编辑器插件:从功能实现到界面美化
  • Spring Boot 集成 Spring AI OpenAI Starter 教程
  • WebPShop 插件:Photoshop 支持 WebP 格式的解决方案
  • FP8 量化技术降低显存需求,实现 AI 绘画低配运行
  • 基于 LVM 部署 MinIO 分布式高可用集群

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online