数据结构——平衡二叉树(2)

平衡二叉树

在顺序存储的数据集中,查找效率主要依赖"有序性";而在链式存储的树结构中,查找效率除了依赖"有序性"外,还高度依赖"形态"。为了避免二叉搜索树在极端插入序列下退化为近似链表,引入一种能在动态插入与删除中"自动保持高度平衡"的二叉搜索树结构,会显著提升最坏情况下的查找与更新效率。基于这一动机,读者需要首先把握平衡二叉树的定义与基本性质,然后循序掌握查找、插入与删除过程中的再平衡机制,最后在代码层面理解实现要点。


1. 基本概念与性质

为了建立对平衡二叉树的统一认识,先从形式化定义入手,再解释关键性质与高度界限,帮助读者将"何为平衡"与"如何判定"清晰对应起来。

平衡二叉树是指在满足二叉搜索树有序性的前提下,对任意结点的左右子树高度差不超过 1 的二叉树。为了便于实现,通常为每个结点维护"平衡因子",并在插入与删除后,通过局部旋转有限次地恢复平衡。下面的图示给出了基本术语与局部判定的关系网络,读者可以据此快速定位各概念之间的依赖关系。
平衡 失衡 平衡二叉树(AVL) 二叉搜索树有序性 平衡性约束 任意结点左右子树高度差≤1 平衡因子BF = h(左) - h(右) 取值范围 BF ∈ {-1, 0, 1} |BF| ≥ 2,触发旋转 局部旋转调整 单旋:LL / RR 双旋:LR / RL 高度与复杂度 高度 O(log n) 查找 / 插入 / 删除平均与最坏均为 O(log n)

在理解了定义之后,考虑到"平衡因子"的直观含义与取值范围的工程实现意义是紧密相关的。维护平衡因子不仅能在常数时间内判断是否需要旋转,还能快速确定旋转类别,从而将再平衡的代价限制在自根向上的一条路径之内。此外,平衡二叉树的高度与结点数量呈对数关系,因此可以保证在最坏情况下,查找与更新的时间复杂度都不会退化。

为了进一步建立直觉,下图展示了一个平衡二叉树示例,并在结点旁标注平衡因子。通过观摩该示意图,读者可以学会"看树识平衡"的基本方法。
4
BF=0 2
BF=0 6
BF=0 1
BF=0 3
BF=0 5
BF=0 7
BF=0

上述示意中,任一结点的左右子树高度差均未超过 1,且作为二叉搜索树,左子树关键字小于该结点关键字,右子树关键字大于该结点关键字。对于实际实现而言,除了保存关键字与指针,往往还会保存"高度"或"平衡因子",二者可互推,选择何者取决于增删操作中更新代价与代码风格的取舍。


2. 查找过程与性能分析

为了在动态集合上实现高效查找,可以沿用二叉搜索树的"比较---分支"策略,同时借助平衡性的保证来控制访问路径的长度。读者可以先回忆二叉搜索树的查找策略,再结合平衡性的高度界限进行最坏复杂度分析。

查找过程按照当前结点的关键字与目标关键字之间的大小关系,逐层向左或向右子树移动。当遇到空指针时表示查找失败;当遇到关键字相等时表示查找成功。与普通二叉搜索树不同的是,平衡二叉树保证了树高为 O(log n),因此查找的比较次数与指针移动次数都具有对数级上界。

为了帮助读者形成操作直观,下面的图示展示了在一棵平衡二叉树中查找关键字"29"的路径。图中对经过的结点进行了路径高亮,便于读者将抽象描述与具体访问序列对应起来。
30 20 40 10 25 35 50 22 28 27 29

从时间复杂度角度看,平衡二叉树的查找在平均与最坏情况下均为 O(log n)。从空间复杂度角度看,相比普通二叉搜索树,平衡二叉树需要为每个结点额外存储一个平衡因子或高度字段,空间开销为 O(n),常数因子可接受。综合考虑,平衡二叉树适合"查找频繁,且动态插入删除也较多"的场景。


3. 插入操作与旋转调整

当集合动态增长时,插入会沿着二叉搜索树的有序性寻找空位置完成挂接。然而新结点的加入可能使某些祖先结点的平衡因子越界,从而需要通过局部旋转将该局部子树重新调整为平衡状态。为了建立系统化的掌握路径,建议按"自下而上更新平衡因子---发现首个失衡祖先---判别四类失衡---执行相应旋转---更新相关结点平衡因子"的顺序来理解。

插入后引发的失衡可归纳为四类:LL、RR、LR、RL。四类分别对应"在某结点的左子树的左子树插入"、"右子树的右子树插入"、"左子树的右子树插入"、"右子树的左子树插入"。LL 与 RR 采用单旋,LR 与 RL 采用双旋。下面通过图示逐一呈现每一类的旋转前后形态变化,并在图后配以简要讲解,帮助读者抓住"失衡方向---旋转方向"的稳定对应关系。

3.1 单旋情形(LL/RR)

为了更直观地理解单旋,请先观察 LL 失衡的局部形态变化过程。图中以"在 A 的左子树的左子树插入新结点"导致 A 的 BF=+2 为例,采用右旋恢复平衡。
右旋后(恢复平衡) Bl B A C Ar 插入前(LL 失衡) B A Ar Bl C

· 上图中,A 为首个失衡祖先,B 是 A 的左孩子,C 是 B 的右子树。右旋以 B 为新根、A 为其右子树,C 上移成为 A 的左子树,从而缩短了 A 的左高并恢复平衡。

· RR 失衡与 LL 对称,处理方式为一次左旋。将"左/右""右旋/左旋"互换即可获得 RR 的示意与实现。

3.2 双旋情形(LR/RL)

当新结点插入到"左子树的右子树"或"右子树的左子树"时,单次旋转不足以同时恢复有序性与平衡性,需要先对子树做一次"内向旋转",再对失衡祖先做一次"外向旋转"。下面以 LR 为例,先对 B 左旋使"内侧"结点 C 上位,再对 A 右旋完成最终恢复。
第二步:对A右旋 C A B Ar 第一步:对B左旋 C B Bl 插入前(LR 失衡) A B Ar Bl C

· RL 情形与 LR 对称,先对右孩子做右旋,再对祖先做左旋。双旋的关键在于把"插入路径上的内侧结点"提升为局部根,随后再用一次外向旋转收拢高度差。

3.3 插入全过程示例

为了把"路径回溯---首个失衡---分类---旋转---更新 BF"的抽象流程具象化,下面给出一次完整插入示例。读者可以依图追踪平衡因子变化与旋转触发时机。
插入22后回溯 30
BF=+1 20
BF=+1 40
BF=0 10
BF=0 25
BF=+1 22
BF=0 初始 30
BF=0 20
BF=0 40
BF=0 10
BF=0 25
BF=0

在该示例中,插入 22 后,沿回溯路径更新了 25、20、30 的平衡因子,但均未达到 ±2,故无需旋转。若继续插入 21,则 25 的左高再增、20 变为"左---右"失衡,应触发 LR 双旋。此时,先对 25 左旋,再对 20 右旋,最后更新相关结点的 BF 值并终止回溯。


4. 删除操作与再平衡

删除操作相较插入更具"多态性",因为被删结点可能是叶子、只有一个孩子或有两个孩子。删除后沿祖先路径向上,高度可能减少,从而引起更"远处"的失衡;但与插入相同的是,再平衡仍局限在自下而上的一条路径上,且每一处失衡仍可用单旋或双旋消除。建议按"定位被删结点---执行删除---自下而上更新 BF---遇到 |BF|≥2 则旋转---继续或终止"的节奏理解。

4.1 删除的三种基本情形

为了把握删除的局部形态差别,可以先按被删结点的度分类,并在"有两个孩子"的情形下使用"后继或前驱替换"的手段将问题化为"删叶或删单支"。这一化约能保证代码结构的稳定与清晰。

(1) 叶子结点删除。

· 直接断开与父结点的指针;父结点高度可能减一,从而需要更新其 BF 并向上回溯。

(2) 单支结点删除。

· 用其唯一的孩子顶替自身位置,等价于"跨过被删结点";随后同样自下而上回溯更新。

(3) 双支结点删除。

· 以中序后继(或前驱)结点替换被删结点的关键字,再将删除转化为"在右子树删除后继叶"(或"在左子树删除前驱叶"),其余步骤与前两类相同。

4.2 删除后的旋转策略

删除导致的失衡也分为以"祖先结点"为中心的四类,但触发时 BF 的符号含义与插入后的判别略有不同。工程实现中,常用的做法是:当发现祖先结点 BF=±2 时,检查"高子树方向的孩子结点"的 BF,若孩子 BF=0、±1,则据此决定是"单旋"还是"先内后外的双旋"。下面给出一个"删除引发 RR 旋转"的局部示意。
左旋恢复 B A Bl Al 删除后(A的右高进一步加强) A Al B Bl 删除前(右高失衡将至) A Al B Bl Br

4.3 删除全过程示例

为了展示"回溯可持续触发多次旋转"的特点,下面给出一个删除关键字并连续再平衡的示意。图中省略若干与旋转无关的兄弟子树,仅保留决定姿态的局部结构。
左旋恢复 70 50 80 60 再次删除后(L 为空) 50
BF=-2 70
BF=0 删除20后回溯 50
BF=-1 30
BF=-1 70
BF=0 40
BF=0 初始平衡 50 30 70 20 40 60 80

在上述序列中,删除两个结点造成根结点右倾失衡,随后一次左旋恢复全树平衡。工程实现中,删除回溯阶段可能出现"多次旋转"的情况,但每次旋转都仅在局部常数范围内完成,整体复杂度仍保持 O(log n)。


5. 实现要点与 C 语言参考代码

在实现层面,平衡二叉树的关键在于"自底向上的信息更新与局部旋转"。为了便于迁移到实际工程,建议采用"结点高度"而非"平衡因子"作为维护字段,因为高度能直接用于判断 BF,同时也有利于在需要时扩展为其他平衡树结构。下面给出一个插入与旋转的参考实现片段,示例关注"字段定义---高度维护---四类旋转---插入过程",删除的实现策略与插入相仿,读者可在此基础上拓展。

c 复制代码
// 平衡二叉树(AVL)结点定义与插入实现(参考示例,便于教材阅读与实验)
// 说明:为突出核心逻辑,未包含内存错误处理与重复键策略等工程细节。

#include <stdio.h>
#include <stdlib.h>

typedef struct AVLNode {
    int key;                    // 关键字
    int height;                 // 以本结点为根的高度
    struct AVLNode *left;       // 左子树
    struct AVLNode *right;      // 右子树
} AVLNode;

// 获取结点高度,空指针高度定义为 0,便于计算
static inline int height(AVLNode *x) {
    return x ? x->height : 0;
}

// 更新结点高度:1 + max(左高, 右高)
static inline void update(AVLNode *x) {
    int hl = height(x->left), hr = height(x->right);
    x->height = (hl > hr ? hl : hr) + 1;
}

// 计算平衡因子:左高 − 右高
static inline int balance_factor(AVLNode *x) {
    return height(x->left) - height(x->right);
}

// 右旋:以 y 为根,x = y->left 上位
//        y                 x
//       / \               / \
//      x   T3   ==>     T1  y
//     / \                   / \
//    T1 T2                 T2 T3
static AVLNode* rotate_right(AVLNode *y) {
    AVLNode *x = y->left;
    AVLNode *T2 = x->right;
    // 旋转
    x->right = y;
    y->left = T2;
    // 更新高度(先下后上)
    update(y);
    update(x);
    return x;
}

// 左旋:以 x 为根,y = x->right 上位
//    x                    y
//   / \                  / \
//  T1  y     ==>        x  T3
//     / \              / \
//    T2 T3            T1 T2
static AVLNode* rotate_left(AVLNode *x) {
    AVLNode *y = x->right;
    AVLNode *T2 = y->left;
    // 旋转
    y->left = x;
    x->right = T2;
    // 更新高度(先下后上)
    update(x);
    update(y);
    return y;
}

// 插入:按 BST 规则递归插入后,自底向上更新并再平衡
AVLNode* avl_insert(AVLNode *root, int key) {
    if (!root) {
        AVLNode *node = (AVLNode*)malloc(sizeof(AVLNode));
        node->key = key;
        node->height = 1;
        node->left = node->right = NULL;
        return node;
    }
    if (key < root->key) {
        root->left = avl_insert(root->left, key);
    } else if (key > root->key) {
        root->right = avl_insert(root->right, key);
    } else {
        // 可根据需求处理"等键":忽略或计数或替换
        return root;
    }

    // 更新高度
    update(root);

    // 计算平衡因子并分类旋转
    int bf = balance_factor(root);

    // LL:左左(右旋)
    if (bf > 1 && key < root->left->key) {
        return rotate_right(root);
    }
    // RR:右右(左旋)
    if (bf < -1 && key > root->right->key) {
        return rotate_left(root);
    }
    // LR:左右(先左旋左子,再右旋本结点)
    if (bf > 1 && key > root->left->key) {
        root->left = rotate_left(root->left);
        return rotate_right(root);
    }
    // RL:右左(先右旋右子,再左旋本结点)
    if (bf < -1 && key < root->right->key) {
        root->right = rotate_right(root->right);
        return rotate_left(root);
    }
    return root; // 无需旋转
}

上述代码体现了"插入即回溯---回溯即再平衡"的模式。每次递归返回时,都会更新当前结点的高度并立即检查是否失衡;一旦发现失衡,即刻按照插入键相对位置判定四类并执行旋转。由于旋转会改变局部根,函数返回值需要作为父结点相应孩子指针的更新来源,从而保持整棵树指针结构的正确性。


6. 综合图示与过程讲解

在掌握了定义、查找、插入与删除的要点之后,将它们放在一条动态演化时间线中观察,会更容易形成"局部旋转如何局部生效、全局高度如何稳定"的整体认识。下面通过一组合成图,把"按序插入---触发双旋---再插入---删除---再平衡"的过程串联展示,读者可沿图逐步体会自下而上的信息流动与旋转触发逻辑。

6.1 连续插入触发 LR 双旋的演进
步骤2:LR 双旋后 25 20 30 步骤1:插入序列 [30, 20, 25] 30 20 null null 25

在该演进中,插入 25 使 30 的左子树更高且内侧结点更深,判定为 LR。先对 20 左旋将 25 提升,再对 30 右旋以 25 为新的局部根,平衡因子即时更新。

6.2 插入若干结点后删除引发 RR 单旋
左旋恢复 30 25 40 20 删除28后回溯 25
BF=-2 20
BF=0 30
BF=-1 40
BF=0 初始 25 20 30 28 40

该序列中,删除 28 让 30 的右子树相对更高,回溯时 25 首先出现 RR 失衡,采用左旋即可恢复平衡。持续回溯若仍出现失衡,可继续以相同策略处理。


7. 工程实践中的若干细节

在教材与实际工程之间,存在一些容易被忽略但非常关键的实现细节。把这些细节提前梳理清楚,有助于减少"读完能懂、下手易错"的落差。

(1) 递归返回值的意义。

· 旋转可能改变子树根结点,必须用"函数返回值"回填给父结点的相应孩子指针,否则结构会断裂。

(2) 高度与平衡因子的选择。

· 维护"高度"较直观,便于扩展;维护"平衡因子"则更新开销略小。两者均可,只需保证更新顺序"先子后父"。

(3) 删除的多次旋转。

· 删除可能沿回溯路径触发多次失衡,需要在每次修正后继续回溯,直至根或遇到 BF 未变化的剪枝条件。

(4) 等关键字策略。

· 常见策略包括"忽略新键""计数频次""允许重复键挂在固定一侧"。应在设计之初统一约定,并在比较逻辑中保持一致。

(5) 迭代实现与尾递归化。

· 在栈资源受限或数据规模较大时,可将递归改为迭代并显式维护访问栈。核心仍是"路径记录---回溯更新---局部旋转"。


8. 小结与延伸阅读提示

在本节的学习中,读者已经完成了从"何谓平衡"到"如何维持平衡"的系统掌握:平衡二叉树在结构上以平衡因子限制左右子树高度差,在操作上以局部旋转做到插入与删除的最坏 O(log n)。通过多幅图示与参考代码,查找、插入、删除与再平衡的关键步骤已经贯通起来。对于进一步的性能比较与工程取舍,可以在掌握本节内容的基础上,继续将"旋转代价""缓存局部性""实现复杂度"等因素纳入考量,并在更大规模的实验中验证结论的稳定性。


**附:一图总览平衡二叉树的核心知识要点。**该图将"定义---判定---旋转---复杂度---实现字段"的主干关系整合在一张思维导图式的结构中,便于复习时快速回忆。
平衡二叉树要点总览 定义:BST + 任意结点 |BF| ≤ 1 判定:BF = h(左) − h(右) ∈ {−1, 0, 1} 失衡:|BF| ≥ 2 触发旋转 插入再平衡:LL / RR 单旋;LR / RL 双旋 删除再平衡:回溯可多次旋转 复杂度:查找 / 插入 / 删除均 O(log n) 实现:维护高度或 BF,递归返回值回填指针

通过以上结构化讲解与多幅可视化图示,读者应能够在较短时间内建立对平衡二叉树的完整工作机理的直观理解,并具备将其应用到实际程序系统中的能力。

相关推荐
小许学java16 小时前
数据结构-模拟实现顺序表和链表
java·数据结构·链表·arraylist·linkedlist·顺序表模拟实现·链表的模拟实现
稚辉君.MCA_P8_Java17 小时前
Gemini永久会员 C++返回最长有效子串长度
开发语言·数据结构·c++·后端·算法
dragoooon3418 小时前
[优选算法专题十.哈希表 ——NO.55~57 两数之和、判定是否互为字符重排、存在重复元素]
数据结构·散列表
稚辉君.MCA_P8_Java19 小时前
Gemini永久会员 go数组中最大异或值
数据结构·后端·算法·golang·哈希算法
会员果汁19 小时前
双向链式队列-C语言
c语言·数据结构
AI科技星20 小时前
张祥前统一场论:引力场与磁矢势的关联,反引力场生成及拉格朗日点解析(网友问题解答)
开发语言·数据结构·经验分享·线性代数·算法
C雨后彩虹20 小时前
最少交换次数
java·数据结构·算法·华为·面试
-森屿安年-20 小时前
二叉平衡树的实现
开发语言·数据结构·c++
稚辉君.MCA_P8_Java20 小时前
Gemini永久会员 Go 返回最长有效子串长度
数据结构·后端·算法·golang
TL滕20 小时前
从0开始学算法——第五天(初级排序算法)
数据结构·笔记·学习·算法·排序算法