从零开始 C++----十【C++ 数据结构】AVL 树详解:从原理到实现

系列文章目录

提示:这里是系列文章的专栏
并不喜欢吃鱼的C++专栏


提示:以下是文章目录哦!

文章目录

目录

系列文章目录

文章目录

前言

[一、AVL 树的核心概念](#一、AVL 树的核心概念)

[1.1 什么是 AVL 树?](#1.1 什么是 AVL 树?)

[1.2 平衡因子](#1.2 平衡因子)

[1.3 AVL 树的性质](#1.3 AVL 树的性质)

[1.3.1 AVL树的时间复杂度](#1.3.1 AVL树的时间复杂度)

[1.3.2 为什么 AVL 树的时间复杂度能稳定在 O (logN)?](#1.3.2 为什么 AVL 树的时间复杂度能稳定在 O (logN)?)

二、AVL树的实现

[1.1 AVL树节点的实现](#1.1 AVL树节点的实现)

[1.2 AVL树的实现](#1.2 AVL树的实现)

[1.3 AVL树的插入的实现](#1.3 AVL树的插入的实现)

[1.3.1 AVL 树插入一个值的整体流程](#1.3.1 AVL 树插入一个值的整体流程)

[1.3.2 平衡因子更新规则](#1.3.2 平衡因子更新规则)

[1.3.3 平衡因子更新的 3 种情况(一定要理解记忆)](#1.3.3 平衡因子更新的 3 种情况(一定要理解记忆))

[情况 1:更新后 BF = 0](#情况 1:更新后 BF = 0)

[情况 2:更新后 BF = 1 或 -1](#情况 2:更新后 BF = 1 或 -1)

[情况 3:更新后 BF = 2 或 -2(失衡)](#情况 3:更新后 BF = 2 或 -2(失衡))

[1.3.4 最终停止更新的条件](#1.3.4 最终停止更新的条件)

[1.3.5 总结](#1.3.5 总结)

[1.3.6 插入功能代码的实现](#1.3.6 插入功能代码的实现)

[1.3.6.1 空树处理](#1.3.6.1 空树处理)

[1.3.6.2 BST 查找插入位置](#1.3.6.2 BST 查找插入位置)

[1.3.6.3 挂新节点并维护父指针](#1.3.6.3 挂新节点并维护父指针)

[1.3.6.4 核心:更新平衡因子的循环](#1.3.6.4 核心:更新平衡因子的循环)

[1.3.7 AVL树的旋转的功能的实现](#1.3.7 AVL树的旋转的功能的实现)

[1.3.7.1 旋转的核心原则](#1.3.7.1 旋转的核心原则)

[1. 右单旋(解决 LL 型失衡)](#1. 右单旋(解决 LL 型失衡))

[2. 左单旋(解决 RR 型失衡)](#2. 左单旋(解决 RR 型失衡))

[3. 左右双旋(解决 LR 型失衡)](#3. 左右双旋(解决 LR 型失衡))

[4. 右左双旋(解决 RL 型失衡)](#4. 右左双旋(解决 RL 型失衡))

[1.3.8 AVL树查找功能的实现](#1.3.8 AVL树查找功能的实现)

[1.3.9 AVL树平衡的检测(这一点了解思路即可)](#1.3.9 AVL树平衡的检测(这一点了解思路即可))

[1.3.10 AVL树的删除](#1.3.10 AVL树的删除)


前言

提示:这里可以添加本文要记录的大概内容:

这是一篇面向C++ 数据结构入门学习者的保姆级教程,承接《搜索二叉树(BST)全解析》,聚焦自平衡二叉搜索树的经典实现 ------AVL 树。目标是帮读者彻底搞懂 AVL 树的核心原理、平衡机制与工程实现,解决普通 BST 在极端场景下的性能退化问题,为后续学习红黑树、数据库索引等进阶内容打下基础


提示:以下是本篇文章正文内容

一、AVL 树的核心概念

1.1 什么是 AVL 树?

AVL树是最先发明的自平衡二叉查找树,AVL是一颗空树,或者具备下列性质的二叉搜索树:它的左右子树都是AVL树,且左右子树的高度差的绝对值不超过1。AVL树是一颗高度平衡搜索二叉树,通过控制高度差去控制平衡
AVL树得名于它的发明者G.M.Adelson-Velsky和E.M.Landis是两个前苏联的科学家,他们在1962年的论文《An algorithm for the organization of information》中发表了它

以上面这张图为例,我们来看"8"这个结点,他的左子树,也就是左箭头连接的全部节点,从上往下数,高度最高为3,右子树高度最高为2,两遍的高度绝对值为1,没有超过1,并且以任意一个结点来看都是这样的


1.2 平衡因子

每个结点都有一个平衡因子,任何结点的平衡因子=右子树高度 - 左子树高度 ,范围∈[-1, 0, 1],AVL树并不是必须要平衡因子,但是有了平衡因子可以更方便我们去进行观察和控制树是否平衡,就像一个风向标一样,也就是说当平衡因子绝对值 > 1时,则会失衡

思考一下为什么AVL树是高度平衡搜索二叉树,要求高度差不超过1,而不是高度差是0呢?0不是更好的平衡吗?画画图分析我们发现,不是不想这样设计,而是有些情况是做不到高度差是0的。比如一棵树是2个结点,4个结点等情况下,高度差最好就是1,无法做到高度差是0

上面这张图红色的节点"10",就是失衡的节点,因为它的平衡因子的绝对值已经超过1了,因此需要进行一定的处理才能使其平衡因子的绝对值降为1或者0,后续我们会讲到旋转,就可以解决这种情况


1.3 AVL 树的性质

1.3.1 AVL树的时间复杂度

查找 / 插入 / 删除的时间复杂度:稳定为 O(logN)

对比普通 BST:普通 BST 最坏会退化成链表,时间复杂度掉到 O(N),而 AVL 树通过平衡约束,彻底避免了这种情况

1.3.2 为什么 AVL 树的时间复杂度能稳定在 O (logN)?

核心原因:

这个约束,让 AVL 树的高度被牢牢限制住了。我们可以用数学推导来证明它的高度上限: 设 AVL 树高度为 h,节点数最少为 N(h),那么:

一、先搞懂一个核心问题:我们为什么要做这个推导?

这里的推理,本质上是在回答:AVL 树最多能 "瘦" 到什么程度?(也就是高度最高能到多少?) 只要我们能算出 "节点数为 N 时,AVL 树的最大高度 h 是多少",就能知道它的时间复杂度上限了

二、第一步:定义「节点数最少的 AVL 树」

我们先定义一个函数: N(h) = 高度为 h 的 AVL 树,最少需要多少个节点

为什么要找 "最少节点数"? 因为节点越少,树就越 "瘦",高度就越高。(这样我们就能花更多的节点专注于树的高度上,而不是丰满度上)只要我们能算出这个 "最瘦" 的 AVL 树的高度上限,那所有 AVL 树的高度都不会超过它

三、第二步:用平衡规则,写出递推公式

AVL 树的核心规则:任意节点的左右子树高度差 ≤ 1 。 我们看一棵高度为 h 的 AVL 树,它的根节点的两个子树,高度只能是这两种情况:

  1. 一个子树高度是 h-1,另一个也是 h-1(完全平衡)
  2. 一个子树高度是 h-1,另一个是 h-2(刚好不失衡的临界状态)

我们要找的是节点最少 的情况,所以会选第二种情况(h-1h-2),因为这样代表花了更多节点在堆高度上,而不是保持平衡上,所以高度为 h 的 AVL 树,最少节点数满足: N(h) = N(h-1) + N(h-2) + 1(这里的1为根节点)

这里的三个部分分别是:

  • N(h-1):高度为 h-1 的子树,最少节点数
  • N(h-2):高度为 h-2 的子树,最少节点数
  • +1:加上根节点本身

四、第三步:算几个小例子,验证一下

我们先算几个简单的情况:

  • h=0(空树):N(0) = 0
  • h=1(只有根节点):N(1) = 1
  • h=2N(2) = N(1) + N(0) + 1 = 1 + 0 + 1 = 2 (一个根节点 + 一个子节点,刚好平衡)
  • h=3N(3) = N(2) + N(1) + 1 = 2 + 1 + 1 = 4 (根节点 + 一个 h=2 的子树 + 一个 h=1 的子树,刚好不失衡)
  • h=4N(4) = N(3) + N(2) + 1 = 4 + 2 + 1 = 7

这个数列 0, 1, 2, 4, 7, 12, ... 是不是和斐波那契数列长得很像?它的增长速度,就是斐波那契数列的增长速度

五、第四步:从递推式,推导出 h 和 N 的关系

这一步有点复杂,我们不过多解释,直接来看结论,大家主要记得是用什么推过来的就行了**,** 我们的 N(h) 数列,本质上是斐波那契数列的变形

六、第五步:用这个结论,解释时间复杂度

我们推导出来的结论是: 节点数为 N 的 AVL 树,最大高度 h ≤ 1.44 * log₂(N)

这意味着:

  • 不管数据怎么分布,AVL 树的高度,始终是 O(logN) 级别的
  • 而查找、插入、删除这些操作,都只需要从根走到叶子,最多走 h
  • 所以,AVL 树的所有操作,时间复杂度都是稳定的 O(logN),绝不会像普通 BST 那样退化成 O(N)

二、AVL树的实现

1.1 AVL树节点的实现

cpp 复制代码
// 这是C++的模板语法,K代表键(key)的类型,V代表值(value)的类型
// 这样写,你的AVL树就可以存任意类型的键值对,比如int-int、string-int等等
template<class K, class V>
// 定义AVL树的节点结构体,名字叫AVLTreeNode
struct AVLTreeNode
{
    // ---------------------- 节点存储的数据 ----------------------
    // pair是C++标准库的键值对类型,_kv用来存这个节点的键和值
    // 比如你插入(5, "apple"),K就是int,V就是string,_kv就存这两个值
    pair<K, V> _kv;

    // ---------------------- 二叉树的基础指针 ----------------------
    // 指向左孩子节点的指针
    // 所有值比当前节点小的键,都会存在左子树里
    AVLTreeNode<K, V>* _left;

    // 指向右孩子节点的指针
    // 所有值比当前节点大的键,都会存在右子树里
    AVLTreeNode<K, V>* _right;

    // 指向父节点的指针
    // 这是AVL树实现的关键!因为插入/删除节点后,我们要沿着父节点一路往上更新平衡因子,没有这个指针会非常麻烦
    AVLTreeNode<K, V>* _parent;

    // ---------------------- AVL树的核心字段:平衡因子 ----------------------
    // _bf 全称是 balance factor,也就是平衡因子
    // 它的定义是:左子树的高度 - 右子树的高度
    // AVL树的规则:所有节点的_bf绝对值必须≤1,否则树就失衡了,需要旋转调整
    // 新节点刚创建时,没有子树,所以_bf初始化为0
    int _bf;


    // ---------------------- 节点的构造函数 ----------------------
    // 当我们创建一个新节点时,会调用这个构造函数
    // 参数kv就是要存的键值对,比如make_pair(3, "banana")
    AVLTreeNode(const pair<K, V>& kv)
        : _kv(kv)       // 把传入的键值对存到节点里
        , _left(nullptr)    // 左孩子初始化为空指针(没有左孩子)
        , _right(nullptr)   // 右孩子初始化为空指针(没有右孩子)
        , _parent(nullptr)  // 父节点初始化为空指针(还没挂到树上)
        , _bf(0)            // 平衡因子初始化为0
    {
        // 构造函数体是空的,所有初始化都在上面的初始化列表里完成了
    }
};

上面备注了详细的注释,不懂的可以仔细看看,这里还是列个表总结一下,并且讲解一下pair这个类型,这里的节点里的数据类型也可以不是一个pair类型,可以是我们常见的单int类型

| 字段 | 作用 | 理解 |
| _kv | 存储键值对数据 | 节点的 "内容",比如你要存的数字和对应的字符串 |
| _left | 指向左孩子 | 所有比当前节点小的数据,都存在左边 |
| _right | 指向右孩子 | 所有比当前节点大的数据,都存在右边 |
| _parent | 指向父节点 | 插入 / 删除后,要沿着父节点往上检查平衡,这是 AVL 树的关键 |

_bf 平衡因子 用来判断树有没有歪掉,歪了就要旋转调整

std::pair 是一个"值对"模板类 template <class T1, class T2> struct pair,它可以把两个不同类型(T1 和 T2)的值打包成一个整体, 这两个值可以通过它的公有成员 `first` 和 `second` 来访问

std::pair 就是一个 "打包工具",能把两个东西绑在一起变成一个整体。 比如你想存 "学号 + 姓名",就可以用 pair<int, string>,把 2025001"张三" 打包成一个变量

cpp 复制代码
#include <utility> // 必须包含这个头文件才能用pair
#include <iostream>
#include <string>

int main() {
    // 1. 创建一个pair:int + string
    std::pair<int, std::string> student;
    
    // 2. 给它的两个成员赋值
    student.first = 2025001;  // 第一个值,对应模板里的T1
    student.second = "张三";  // 第二个值,对应模板里的T2
    
    // 3. 访问它的值
    std::cout << "学号:" << student.first << std::endl;
    std::cout << "姓名:" << student.second << std::endl;

    // 也可以用make_pair快速创建
    auto p = std::make_pair(3.14, "圆周率");
    std::cout << p.first << " = " << p.second << std::endl;
    return 0;
}

为什么 AVL 树里要用它?

之前写的 AVL 树节点里,用了 pair<K, V> _kv,就是把 "键(key)和值(value)" 打包在一起: K 是键的类型(比如 int,用来排序和查找) V 是值的类型(比如 string,用来存数据) 这样一个节点就能同时存 "用来排序的 key" 和 "要保存的数据 value",非常方便


1.2 AVL树的实现

cpp 复制代码
// 这是C++的模板声明,K代表键(key)的类型,V代表值(value)的类型
// 这样写,你的AVL树就可以支持任意类型的键值对,比如int-int、string-int等
template<class K, class V>
// 定义AVL树的主类,名字叫AVLTree
class AVLTree
{
    // typedef 是给类型起别名
    // 这里把 AVLTreeNode<K, V> 这个节点类型,起了个短别名叫 Node
    // 后面代码里写 Node* 就等价于写 AVLTreeNode<K, V>*,简化代码书写
    typedef AVLTreeNode<K, V> Node;

public:
    // 这里是类的公有成员,后面我们会在这里写插入、删除、查找等对外接口
    // 现在先用//...占位,后续可以继续补充实现
    //...

private:
    // 这里是类的私有成员,外部不能直接访问,只能通过类的成员函数操作
    // _root 是AVL树的根节点指针,初始化为nullptr(空树)
    // 整个AVL树的所有节点,都是通过根节点来访问的
    Node* _root = nullptr;
};

由于注释很详细,这里不展开赘述


1.3 AVL树的插入的实现

1.3.1 AVL 树插入一个值的整体流程

  • 按二叉搜索树(BST)规则插入新节点 比当前节点小往左走,比当前节点大往右走,找到空位置插入

  • 更新平衡因子 插入新节点后,只会影响从新节点到根节点这条路径上的祖先节点 。 我们需要沿着这条路径向上更新所有祖先的平衡因子

  • 判断是否失衡

    • 如果更新过程中没有出现不平衡,插入直接结束。
    • 如果更新过程中出现不平衡,对不平衡的子树进行旋转。
    • 旋转后子树恢复平衡,高度也会恢复到插入前,不会再影响上层,插入结束

1.3.2 平衡因子更新规则

1. 平衡因子定义

2. 更新原则

  • 只有子树高度发生变化,才会影响当前节点的平衡因子
  • 插入节点会导致子树高度增加:
    • 新节点插在 parent 左子树 → parent 的平衡因子 −−
    • 新节点插在 parent 右子树 → parent 的平衡因子 ++

1.3.3 平衡因子更新的 3 种情况(一定要理解记忆)

情况 1:更新后 BF = 0
  • 变化:-1 → 01 → 0
  • 含义: 之前子树一边高一边低, 新节点插在了矮的一边, 现在两边一样高了。
  • 结果: 子树高度不变 → 不会影响上层 → 更新停止,插入结束

更新到中间结点,3为根的子树高度不变,不会影响上一层,更新结束


情况 2:更新后 BF = 1 或 -1
  • 变化:0 → 10 → -1
  • 含义: 之前两边一样高, 新节点插在某一边, 现在一边高一边低,但仍然合法
  • 结果: 子树高度变高了 → 会影响上层 → 必须继续向上更新

情况 3:更新后 BF = 2 或 -2(失衡)
  • 变化:1 → 2-1 → -2
  • 含义: 之前一边已经更高, 新节点又插在了更高的一边, 导致平衡被破坏。
  • 结果: 必须旋转调整 旋转目标:
    1. 让子树恢复平衡
    2. 让子树高度恢复到插入之前 旋转完成后 → 不再影响上层 → 更新停止,插入结束

更新到10结点,平衡因子为2,10所在的子树已经不平衡,需要旋转处理


1.3.4 最终停止更新的条件

满足任意一个就停止向上更新:

  1. 更新到 根节点
  2. 更新后 BF = 0
  3. 更新后 BF = ±2(旋转后停止)

1.3.5 总结

插入按 BST 规则走,插入后向上更新平衡因子:

  • BF=0 → 停
  • BF=±1 → 继续向上
  • BF=±2 → 旋转 → 停

接下来是插入代码的实现,如果上面你搞懂了,下面也不是很难,看一下就懂了

1.3.6 插入功能代码的实现

----------------------------------------------接下来是插入功能的实现------------------------------------------------

cpp 复制代码
// 2.2.3 插入结点及更新平衡因子的代码实现
template<class K, class V>
bool AVLTree<K, V>::Insert(const pair<K, V>& kv)
{
    // 1. 空树特殊处理:直接把新节点作为根节点
    if (_root == nullptr)
    {
        _root = new Node(kv);
        return true;
    }

    // 2. 按BST规则找到插入位置
    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;
        }
        // 等于当前节点 → 不允许重复插入,直接返回false
        else
        {
            return false;
        }
    }

    // 3. 创建新节点,挂到父节点上
    cur = new Node(kv);
    if (parent->_kv.first < kv.first)
    {
        // 新节点比父节点大 → 挂在父节点的右边
        parent->_right = cur;
    }
    else
    {
        // 新节点比父节点小 → 挂在父节点的左边
        parent->_left = cur;
    }
    cur->_parent = parent; // 新节点的父指针指向parent

    // --------------------------
    // 4. 关键:更新平衡因子
    // 从cur的父节点开始,一路向上更新到根
    // --------------------------
    while (parent)
    {
        // 4.1 根据新节点的位置,更新父节点的平衡因子
        if (cur == parent->_left)
        {
            // 新节点插在左子树 → 左子树高度+1 → 平衡因子-1
            parent->_bf--;
        }
        else
        {
            // 新节点插在右子树 → 右子树高度+1 → 平衡因子+1
            parent->_bf++;
        }

        // 4.2 根据更新后的平衡因子,判断是否需要继续向上更新
        if (parent->_bf == 0)
        {
            // 情况1:平衡因子变为0 → 子树高度不变,不会影响上层
            // 停止更新,插入结束
            break;
        }
        else if (parent->_bf == 1 || parent->_bf == -1)
        {
            // 情况2:平衡因子为±1 → 子树高度+1,会影响上层
            // 继续向上更新
            cur = parent;
            parent = parent->_parent;
        }
        else if (parent->_bf == 2 || parent->_bf == -2)
        {
            // 情况3:平衡因子为±2 → 树失衡了,需要旋转调整
            // 旋转处理逻辑后续实现,这里先break
            break;
        }
        else
        {
            // 理论上不会走到这里,防止出现异常情况
            assert(false);
        }
    }

    return true;
}
1.3.6.1 空树处理

如果树是空的,直接把新节点设为根节点即可,没有父节点,也不需要更新平衡因子

1.3.6.2 BST 查找插入位置
  • 这部分和普通 BST 的插入逻辑完全一样,核心是找到第一个空的位置
  • parent记录 cur 的父节点,方便后续挂新节点
  • 如果遇到相同的 key,直接返回 false(不允许重复 key)
1.3.6.3 挂新节点并维护父指针
  • 根据parentkv的大小关系,决定新节点挂在左边还是右边
  • 关键是要设置cur->_parent = parent,为后续更新平衡因子做准备
1.3.6.4 核心:更新平衡因子的循环
  • 循环的核心逻辑就是你之前整理的三种停止条件
  • 循环里用curparent一路向上走,每次更新父节点的平衡因子
  • 遇到±2时 break,接下来就要写旋转的代码了

----------------------------------------------接下来是旋转功能的实现------------------------------------------------

这里才是最有难度的一点,一定要理解,而非死记

1.3.7 AVL树的旋转的功能的实现

1.3.7.1 旋转的核心原则

旋转操作必须同时满足两个条件:

  1. 保持 BST 的有序性:旋转后树的中序遍历序列不变,仍满足 "左子树 < 根 < 右子树" 的规则
  2. 恢复平衡并降低高度:让失衡的子树重新满足 AVL 平衡条件(平衡因子绝对值≤1),同时将子树高度恢复到插入前的水平,避免继续影响上层节点

AVL 树的旋转分为四种:右单旋、左单旋、左右双旋、右左双旋,其中单旋是基础,双旋是两次单旋的组合


单旋是解决 "问题出在同侧" 的失衡(LL/RR),也是双旋的基础。

1. 右单旋(解决 LL 型失衡)

触发条件

失衡节点 parent 的平衡因子为 -2,且它的左孩子 subL 的平衡因子为 -1(或 0)。 说明:左子树的左分支太高了,需要向右 "掰直"。

核心逻辑(3 步走)

  1. 让左孩子上位subL 成为新的根节点,替代 parent
  2. 过继中间子树 :把 subL 的右子树 subLR,挂到 parent 的左孩子位置。
  3. 原根退位parent 成为 subL 的右孩子。

关键效果

  • 不破坏 BST 规则:中序遍历顺序不变。
  • 平衡因子恢复:parentsubL 的 BF 都会变成 0。
  • 高度恢复:子树高度回到插入前,不会再影响上层。

特殊情况:

各种情况:

情况 1 - 插入前 a/b/c 高度 h = 0

这是最简单的情况,所有子树都是空树(高度为 0)。

步骤拆解

  1. 初始状态

    • 节点 10 的 BF=-1,节点 5 的 BF=0,a/b/c都是空树(h=0)
    • 结构:10 为根,左孩子是 5,5 的左右子树都是空树,10 的右子树c是空树。
  2. 插入新节点后(失衡)

    • a(5 的左子树)插入新节点(值 = 1),a 的高度从 0 变成 1
    • 更新平衡因子:节点 5 的 BF 从 0→-1,节点 10 的 BF 从 - 1→-2(失衡)
  3. 右单旋操作

    • 把节点 5 "抬上来" 成为新根,节点 10 变成它的右孩子
    • 节点 5 原来的右子树b(空树),变成节点 10 的左子树
  4. 最终效果

    • 节点 5 的 BF=0,节点 10 的 BF=0,树恢复平衡
    • 整体高度回到插入前的 h+2 = 0+2 = 2

情况 2 - 插入前 a/b/c 高度 h = 1

这是高度为 1 的子树,包含叶子节点,更接近真实场景。

步骤拆解

  1. 初始状态

    • 节点 10 的 BF=-1,节点 5 的 BF=0
    • a是节点 1(h=1),b是节点 8(h=1),c是节点 15(h=1)
  2. 插入新节点后(失衡)

    • a(节点 1 的左子树)插入新节点(值 =-3),a 的高度从 1 变成 2
    • 更新平衡因子:节点 1 的 BF=0,节点 5 的 BF 从 0→-1,节点 10 的 BF 从 - 1→-2(失衡)
  3. 右单旋操作

    • 节点 5 成为新根,节点 10 变成它的右孩子
    • 节点 5 的右子树b(节点 8),变成节点 10 的左子树
  4. 最终效果

    • 节点 5 的 BF=0,节点 1 的 BF=-1,节点 10 的 BF=0
    • 所有节点的 BF 绝对值≤1,树恢复平衡
    • 整体高度回到插入前的 h+2 = 1+2 = 3

情况 3 - 插入前 a/b/c 高度 h = 2(AVL 子树)

这是高度为 2 的 AVL 子树,用来证明 "无论子树是什么形态的 AVL 树,右单旋都能生效"。

关键说明

  • 图中 x/y/z 代表高度为 2 的 AVL 子树的三种形态(满二叉树、单分支等)
  • 文字部分解释:a必须是x型子树,因为只有这种形态插入节点后,才会直接引发上层节点失衡,而不是自身先旋转。
  • 所有可能的组合:bc可以是x/y/z的任意组合,共 3×3×4=36 种场景,右单旋都能处理。

这里来解释一下3*3*4是怎么来的,由于子树b和c上面三种可以任选一种,因此就是3*3,而我插入一个节点由于只能插入子树a上,因此可以插入左子树的左右两边,或者右子树的左右两边,也就是4种,因此是3*3*4共36种

核心逻辑

  • 无论 a/b/c 是什么形态的 AVL 子树,右单旋的操作都是固定的:
    1. subL(节点 5)上位为新根
    2. subL的右子树b过继给parent(节点 10)
    3. parent成为subL的右孩子
  • 操作后,树必然恢复平衡,且高度回到插入前。

图 4:情况 4 - 插入前 a/b/c 高度 h = 3(AVL 子树)

这是高度为 3 的 AVL 子树,是这组图的终极通用证明。

关键说明

  • 图中 x/y-C 代表高度为 3 的 AVL 子树的 14 种形态
  • 文字部分解释:所有可能的组合有 5400 种,但无论哪种,右单旋的逻辑都不变。
  • 这里用抽象的矩形表示子树,说明:我们不需要关心子树内部的细节,只要它是高度为 h 的 AVL 树,右单旋就一定有效

核心结论

  • 无论 h 是 0/1/2/3/...,右单旋的操作流程完全一致:
    1. subL 上位
    2. subLR 过继
    3. parent 退位
  • 最终结果也一致:树恢复平衡,高度回到 h+2,不会再影响上层节点。

----------------------------------------------接下来右单旋代码的实现------------------------------------------------

cpp 复制代码
// 右单旋:解决 LL 型失衡(parent 的平衡因子为 -2)
// 参数 parent:失衡的节点(要被旋转下去的节点)
template<class K, class V>
void AVLTree<K, V>::RotateR(Node* parent)
{
    // ---------------------- 1. 准备工作:获取关键节点 ----------------------
    // subL:parent 的左孩子,也就是要上位成为新根的节点
    Node* subL = parent->_left;
    // subLR:subL 的右孩子,中间子树,后续要过继给 parent
    Node* subLR = subL->_right;

    // ---------------------- 2. 过继中间子树 subLR 给 parent ----------------------
    // 把 parent 的左孩子,指向 subLR(相当于 subL 把自己的右孩子"过继"给 parent)
    parent->_left = subLR;
    // 如果 subLR 不为空,要把它的父指针改成 parent(维护双向指针)
    if (subLR)
        subLR->_parent = parent;

    // ---------------------- 3. 让 subL 上位,parent 退位 ----------------------
    // 先记录 parent 的父节点(后续要让上层节点指向 subL)
    Node* parentParent = parent->_parent;

    // 让 subL 的右孩子指向 parent(parent 变成 subL 的右子树)
    subL->_right = parent;
    // 让 parent 的父指针指向 subL
    parent->_parent = subL;

    // ---------------------- 4. 处理上层节点的指向 ----------------------
    // parent 可能是整棵树的根,也可能是某个局部子树的根
    if (parentParent == nullptr)
    {
        // 情况1:parent 原来是整棵树的根,旋转后 subL 成为新的根
        _root = subL;
        // 新根的父节点为空
        subL->_parent = nullptr;
    }
    else
    {
        // 情况2:parent 是局部子树,需要修改 parentParent 的孩子指针
        if (parent == parentParent->_left)
        {
            // 如果 parent 是 parentParent 的左孩子,就把 parentParent 的左孩子改成 subL
            parentParent->_left = subL;
        }
        else
        {
            // 如果 parent 是 parentParent 的右孩子,就把 parentParent 的右孩子改成 subL
            parentParent->_right = subL;
        }
        // 让 subL 的父指针指向 parentParent
        subL->_parent = parentParent;
    }

    // ---------------------- 5. 更新平衡因子 ----------------------
    // 右单旋后,parent 和 subL 的平衡因子都会变成 0
    parent->_bf = 0;
    subL->_bf = 0;
}

关键细节说明

  1. 为什么要维护 _parent 指针? 因为 AVL 树是双向指针结构,修改了 _left/_right 之后,必须同步修改 _parent,否则会出现 "孤儿节点" 或指针混乱。

  2. 为什么要处理 parentParent 如果 parent 是整棵树的根,旋转后要把 _root 改成 subL;如果是局部子树,要让它的父节点指向新的根 subL,否则树会断链。

  3. 为什么直接把 BF 设为 0? 在 LL 型失衡的右单旋场景下,旋转后 parentsubL 的平衡因子一定会变成 0,所以可以直接赋值,不用复杂计算。


2. 左单旋(解决 RR 型失衡)

触发条件

失衡节点 parent 的平衡因子为 +2,且它的右孩子 subR 的平衡因子为 +1(或 0)。 说明:右子树的右分支太高了,需要向左 "掰直"。

核心逻辑(3 步走)

  1. 让右孩子上位subR 成为新的根节点,替代 parent
  2. 过继中间子树 :把 subR 的左子树 subRL,挂到 parent 的右孩子位置。
  3. 原根退位parent 成为 subR 的左孩子。

关键效果

  • 不破坏 BST 规则:中序遍历顺序不变。
  • 平衡因子恢复:parentsubR 的 BF 都会变成 0。
  • 高度恢复:子树高度回到插入前,不会再影响上层。

特殊情况:

第 1 步:失衡发生(插入后)

  • 初始状态:
    • parent(10) 的 BF=1(右子树比左子树高 1)
    • subR(15) 的 BF=0(左右子树一样高)
  • 插入操作:在 subR 的右子树 a 中插入新节点,a 的高度从 h 变成 h+1
  • 结果:
    • subR 的 BF 从 0→+1
    • parent 的 BF 从 1→+2(失衡,RR 型)

第 2 步:左单旋操作(核心)

  1. subR 上位subR(15) 成为新的根节点,替代 parent(10)
  2. 过继中间子树 subRL :把 subR 的左子树 subRL,挂到 parent(10) 的右孩子位置
  3. parent 退位parent(10) 变成 subR(15) 的左孩子

第 3 步:旋转后效果

  • 新的根是 subR(15),左孩子是 parent(10),右孩子是 a
  • parent(10) 的右孩子是 subRL,BF 恢复为 0
  • subR(15) 的 BF 恢复为 0
  • 树的整体高度回到插入前的 h+2,不会再影响上层节点

一般情况和右单旋的一样,图是对称的,如果你懂右单旋,一定就懂左单旋

----------------------------------------------接下来左单旋代码的实现------------------------------------------------

cpp 复制代码
// 左单旋:解决 RR 型失衡(失衡节点 parent 的平衡因子为 +2)
// 核心作用:把失衡的"右右型"子树,向左旋转,恢复平衡
template<class K, class V>
void AVLTree<K, V>::RotateL(Node* parent)
{
    // ---------------------- 1. 准备关键节点 ----------------------
    // subR:失衡节点 parent 的右孩子,旋转后会成为新的根节点
    Node* subR = parent->_right;
    // subRL:subR 的左孩子(中间子树,后续要过继给 parent)
    // 这部分子树的值介于 parent 和 subR 之间,旋转后必须保持在中间
    Node* subRL = subR->_left;

    // ---------------------- 2. 过继中间子树 subRL 给 parent ----------------------
    // 让 parent 的右孩子,指向 subRL(相当于 subR 把自己的左孩子"过继"给 parent)
    parent->_right = subRL;
    // 如果 subRL 不为空,要同步修改它的父指针,指向 parent
    if (subRL)
        subRL->_parent = parent;

    // ---------------------- 3. 让 subR 上位,parent 退位 ----------------------
    // 先记录 parent 的父节点(后续要让上层节点指向新根 subR)
    Node* parentParent = parent->_parent;

    // 让 subR 的左孩子指向 parent(parent 变成 subR 的左子树)
    subR->_left = parent;
    // 让 parent 的父指针指向 subR,维护双向指针关系
    parent->_parent = subR;

    // ---------------------- 4. 处理上层节点的指向(关键细节) ----------------------
    // parent 可能是整棵树的根,也可能是某个局部子树的根,需要分情况处理
    if (parentParent == nullptr)
    {
        // 情况1:parent 原来是整棵树的根,旋转后 subR 成为新的根
        _root = subR;
        // 新根的父节点必须为空
        subR->_parent = nullptr;
    }
    else
    {
        // 情况2:parent 是局部子树,需要修改 parentParent 的孩子指针,让它指向 subR
        if (parent == parentParent->_left)
        {
            // 如果 parent 是 parentParent 的左孩子,就把 parentParent 的左孩子改成 subR
            parentParent->_left = subR;
        }
        else
        {
            // 如果 parent 是 parentParent 的右孩子,就把 parentParent 的右孩子改成 subR
            parentParent->_right = subR;
        }
        // 让 subR 的父指针指向 parentParent,维护双向指针关系
        subR->_parent = parentParent;
    }

    // ---------------------- 5. 更新平衡因子 ----------------------
    // RR 型失衡经过左单旋后,parent 和 subR 的平衡因子都会变成 0
    // 因为子树高度恢复到了插入前的水平,不再失衡
    parent->_bf = 0;
    subR->_bf = 0;
}

双旋是解决 "问题出在异侧" 的失衡(LR/RL),本质是两次单旋的组合。

3. 左右双旋(解决 LR 型失衡)

触发条件

失衡节点 parent 的平衡因子为 -2,且它的左孩子 subL 的平衡因子为 +1。 说明:左子树的右分支太高了,直接右单旋会破坏结构,需要先把它变成 LL 型,再右单旋。

核心逻辑(两步走)

  1. 先对 subL 做左单旋:把 "左子树的右分支过高",变成 "左子树的左分支过高"(即把 LR 型转成 LL 型)。
  2. 再对 parent 做右单旋:用右单旋解决 LL 型失衡。

关键效果

  • 两次旋转后,树恢复平衡,且高度回到插入前。
  • 中间节点的平衡因子需要特殊处理,不一定都是 0

情况 1:h=0(插入前子树高度为 0)

  1. 初始状态
    • 节点 10(parent)的 BF=-1,节点 5(subL)的 BF=0
    • 5 的右子树是空(h=0),10 的右子树是空
  2. 插入后失衡
    • 在 5 的右子树插入节点 8,5 的 BF 从 0→+1,10 的 BF 从 - 1→-2(LR 型失衡)
  3. 第一步:对 5 做左单旋
    • 把节点 8 抬上来,节点 5 变成它的左孩子,节点 8 成为新的 subL
    • 此时结构变成了典型的 LL 型失衡
  4. 第二步:对 10 做右单旋
    • 节点 8 成为新根,节点 10 变成它的右孩子,恢复平衡
    • 最终节点 5、8、10 的 BF 都为 0

情况 2:h=1(插入前子树高度为 1)

  1. 初始状态
    • 节点 10(parent)的 BF=-1,节点 5(subL)的 BF=0
    • 5 的左右子树是 1 和 8(h=1),10 的右子树是 15(h=1)
  2. 插入后失衡
    • 在 8 的右子树插入节点 9,8 的 BF 从 0→+1,5 的 BF 从 0→+1,10 的 BF 从 - 1→-2(LR 型失衡)
  3. 第一步:对 5 做左单旋
    • 把节点 8 抬上来,节点 5 变成它的左孩子,节点 8 成为新的 subL
    • 此时结构变成了典型的 LL 型失衡
  4. 第二步:对 10 做右单旋
    • 节点 8 成为新根,节点 10 变成它的右孩子,恢复平衡
    • 最终节点 5 的 BF=-1,节点 8 和 10 的 BF 为 0

这张图是左右双旋的终极通用模型,证明无论子树的初始高度 h 是多少,也无论新节点插在 subLR 的左还是右分支,两次旋转都能完美恢复平衡

分场景拆解(3 种插入位置)

场景 1:新节点插在 subLR 的左子树 e 中

  1. 插入后,subLR 的 BF 从 0→-1,subL 的 BF 从 0→+1,parent 的 BF 从 - 1→-2
  2. 对 subL 做左单旋:subLR 上位为新 subL,subL 变成它的左孩子
  3. 对 parent 做右单旋:subLR 上位为新根,parent 变成它的右孩子
  4. 最终 BF:subL (5)=0,subLR (8)=0,parent (10)=-1

场景 2:新节点插在 subLR 的右子树 f 中

  1. 插入后,subLR 的 BF 从 0→+1,subL 的 BF 从 0→+1,parent 的 BF 从 - 1→-2
  2. 对 subL 做左单旋:subLR 上位为新 subL,subL 变成它的左孩子
  3. 对 parent 做右单旋:subLR 上位为新根,parent 变成它的右孩子
  4. 最终 BF:subL (5)=-1,subLR (8)=0,parent (10)=0

场景 3:subLR 本身就是新节点(h=0)

  1. 插入后,subL 的 BF 从 0→+1,parent 的 BF 从 - 1→-2
  2. 对 subL 做左单旋:subLR 上位为新 subL,subL 变成它的左孩子
  3. 对 parent 做右单旋:subLR 上位为新根,parent 变成它的右孩子
  4. 最终 BF:subL (5)=0,subLR (8)=0,parent (10)=0

----------------------------------------------接下来左右单旋代码实现------------------------------------------------

cpp 复制代码
// 左右双旋:解决 LR 型失衡(失衡节点 parent 的平衡因子为 -2,且其左孩子 subL 的平衡因子为 +1)
// 核心逻辑:先对 subL 做左单旋(把 LR 型转成 LL 型),再对 parent 做右单旋
template<class K, class V>
void AVLTree<K, V>::RotateLR(Node* parent)
{
    // ---------------------- 1. 准备关键节点 ----------------------
    // subL:失衡节点 parent 的左孩子(第一次旋转的对象)
    Node* subL = parent->_left;
    // subLR:subL 的右孩子(两次旋转后会成为新的根节点)
    Node* subLR = subL->_right;
    // 提前保存 subLR 旋转前的平衡因子,用于后续更新
    // 因为两次旋转会修改 subLR 的值,所以必须提前存下来
    int bf = subLR->_bf;

    // ---------------------- 2. 第一次旋转:对 subL 做左单旋 ----------------------
    // 把 LR 型失衡(左子树的右分支过高),转成 LL 型失衡(左子树的左分支过高)
    RotateL(parent->_left);

    // ---------------------- 3. 第二次旋转:对 parent 做右单旋 ----------------------
    // 用右单旋解决 LL 型失衡,恢复整棵树的平衡
    RotateR(parent);

    // ---------------------- 4. 根据 bf 的值,更新三个关键节点的平衡因子 ----------------------
    // bf 是 subLR 旋转前的平衡因子,它的值决定了三个节点最终的 BF 状态
    if (bf == 0)
    {
        // 情况1:新节点就是 subLR 本身(h=0的场景)
        // 旋转后 subL、subLR、parent 的 BF 都为 0
        subL->_bf = 0;
        subLR->_bf = 0;
        parent->_bf = 0;
    }
    else if (bf == -1)
    {
        // 情况2:新节点插在 subLR 的左子树中
        // 旋转后 parent 的右子树更高,BF=1;subL 和 subLR 的 BF 为 0
        subL->_bf = 0;
        subLR->_bf = 0;
        parent->_bf = 1;
    }
    else if (bf == 1)
    {
        // 情况3:新节点插在 subLR 的右子树中
        // 旋转后 subL 的左子树更高,BF=-1;subLR 和 parent 的 BF 为 0
        subL->_bf = -1;
        subLR->_bf = 0;
        parent->_bf = 0;
    }
    else
    {
        // 理论上不会走到这里,防止异常情况
        assert(false);
    }
}

4. 右左双旋(解决 RL 型失衡)

触发条件

失衡节点 parent 的平衡因子为 +2,且它的右孩子 subR 的平衡因子为 -1

说明:右子树的左分支太高了,直接左单旋会破坏结构,需要先把它变成 RR 型,再左单旋。

核心逻辑(两步走)

  1. 先对 subR 做右单旋:把 "右子树的左分支过高",变成 "右子树的右分支过高"(即把 RL 型转成 RR 型)
  2. 再对 parent 做左单旋:用左单旋解决 RR 型失衡

关键效果

  • 两次旋转后,树恢复平衡,且高度回到插入前。
  • 中间节点的平衡因子需要特殊处理,不一定都是 0。

1. 场景 1:新节点插在 subRL 的左子树 e

  1. 失衡发生
    • 初始:parent(10) BF=1,subR(15) BF=0,subRL(12) BF=0
    • e 中插入节点,subRL 的 BF 从 0→-1,subR 的 BF 从 0→-1,parent 的 BF 从 1→+2(RL 型失衡)
  2. 第一步:对 subR 做右单旋
    • subRL(12) 上位成为新的 subRsubR(15) 变成它的右孩子
    • 此时结构变成典型的 RR 型失衡
  3. 第二步:对 parent 做左单旋
    • subRL(12) 上位成为整棵树的新根,parent(10) 变成它的左孩子,subR(15) 变成它的右孩子
  4. 最终 BF 状态parent(10) BF=1,subRL(12) BF=0,subR(15) BF=0

2. 场景 2:新节点插在 subRL 的右子树 f

  1. 失衡发生
    • f 中插入节点,subRL 的 BF 从 0→+1,subR 的 BF 从 0→-1,parent 的 BF 从 1→+2(RL 型失衡)
  2. 第一步:对 subR 做右单旋
    • subRL(12) 上位成为新的 subRsubR(15) 变成它的右孩子
  3. 第二步:对 parent 做左单旋
    • subRL(12) 上位成为新根,parent(10) 变成它的左孩子,subR(15) 变成它的右孩子
  4. 最终 BF 状态parent(10) BF=0,subRL(12) BF=0,subR(15) BF=-1

3. 场景 3:subRL 本身就是新节点(h=0 的情况)

  1. 失衡发生
    • 直接插入 subRL(12)subR 的 BF 从 0→-1,parent 的 BF 从 1→+2(RL 型失衡)
  2. 第一步:对 subR 做右单旋
    • subRL(12) 上位成为新的 subRsubR(15) 变成它的右孩子
  3. 第二步:对 parent 做左单旋
    • subRL(12) 上位成为新根,parent(10) 变成它的左孩子,subR(15) 变成它的右孩子
  4. 最终 BF 状态parent(10) BF=0,subRL(12) BF=0,subR(15) BF=0

----------------------------------------------接下来右左单旋代码实现------------------------------------------------

cpp 复制代码
// 右左双旋:解决 RL 型失衡(parent BF=+2,subR BF=-1)
template<class K, class V>
void AVLTree<K, V>::RotateRL(Node* parent)
{
    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    // 提前保存 subRL 旋转前的平衡因子,用于后续更新
    int bf = subRL->_bf;

    // 1. 先对 subR 做右单旋,把 RL 型转成 RR 型
    RotateR(parent->_right);

    // 2. 再对 parent 做左单旋,解决 RR 型失衡
    RotateL(parent);

    // 3. 根据 bf 的值更新平衡因子
    if (bf == 0)
    {
        // 新节点就是 subRL 本身
        parent->_bf = 0;
        subRL->_bf = 0;
        subR->_bf = 0;
    }
    else if (bf == 1)
    {
        // 新节点插在 subRL 的右子树 f 中
        parent->_bf = 0;
        subRL->_bf = 0;
        subR->_bf = -1;
    }
    else if (bf == -1)
    {
        // 新节点插在 subRL 的左子树 e 中
        parent->_bf = 1;
        subRL->_bf = 0;
        subR->_bf = 0;
    }
    else
    {
        assert(false);
    }
}

1.3.8 AVL树查找功能的实现

太简单了,不过多解释

cpp 复制代码
// 查找函数:根据 key 在 AVL 树中查找对应的节点
// 参数 key:要查找的键
// 返回值:找到的节点指针;没找到返回 nullptr
template<class K, class V>
Node* AVLTree<K, V>::Find(const K& key)
{
    // cur:从根节点开始遍历
    Node* cur = _root;

    // 只要当前节点不为空,就继续查找
    while (cur)
    {
        // 如果当前节点的 key 小于目标 key,说明目标在右子树
        if (cur->_kv.first < key)
        {
            cur = cur->_right;
        }
        // 如果当前节点的 key 大于目标 key,说明目标在左子树
        else if (cur->_kv.first > key)
        {
            cur = cur->_left;
        }
        // 如果相等,说明找到了目标节点,直接返回
        else
        {
            return cur;
        }
    }

    // 遍历结束仍未找到,返回空指针
    return nullptr;
}

1.3.9 AVL树平衡的检测(这一点了解思路即可)

我们实现的AVAVL树是否合格,我们通过检查左右子树高度差的的程序进行反向验证,同时检查一下结点的平衡因子更新是否出现了问题

功能说明

这个函数会检查三件事:

  1. 是否是二叉搜索树 BST(左 < 根 < 右)
  2. 每个节点的平衡因子是否正确(= 右高 - 左高)
  3. 是否满足 AVL 平衡条件(所有节点 |BF| ≤ 1)
cpp 复制代码
// 检查以 cur 为根的子树是否满足 AVL 树规则
// 返回值:子树的高度(用于递归计算)
// 如果不平衡,直接返回 -1 或 断言报错
template<class K, class V>
int AVLTree<K, V>::_IsBalance(Node* cur)
{
    // 1. 递归终止条件:空树高度为 0,一定平衡
    if (cur == nullptr)
    {
        return 0;
    }

    // 2. 递归计算 左子树高度 和 右子树高度
    int leftH = _IsBalance(cur->_left);   // 左子树高度
    int rightH = _IsBalance(cur->_right); // 右子树高度

    // ------------------------------------------------------------------
    // 3. 检查:当前节点的平衡因子 是否等于 右高 - 左高
    // 这一步是为了验证我们插入/旋转时更新的 BF 是否正确
    // ------------------------------------------------------------------
    if (cur->_bf != rightH - leftH)
    {
        cout << "平衡因子错误!" << endl;
    }

    // ------------------------------------------------------------------
    // 4. 核心:检查 AVL 平衡条件
    // 平衡因子绝对值 必须 ≤ 1
    // ------------------------------------------------------------------
    if (abs(cur->_bf) > 1)
    {
        cout << "节点失衡!key = " << cur->_kv.first << endl;
    }

    // ------------------------------------------------------------------
    // 5. 检查 BST 性质(左子树 < 根,右子树 > 根)
    // ------------------------------------------------------------------
    // 左孩子存在 && 左孩子值 >= 当前节点值 → 不是BST
    if (cur->_left && cur->_left->_kv.first >= cur->_kv.first)
    {
        cout << "BST 规则破坏(左孩子大于根)" << endl;
    }
    // 右孩子存在 && 右孩子值 <= 当前节点值 → 不是BST
    if (cur->_right && cur->_right->_kv.first <= cur->_kv.first)
    {
        cout << "BST 规则破坏(右孩子小于根)" << endl;
    }

    // 6. 返回当前子树的高度 = max(左高, 右高) + 1
    return max(leftH, rightH) + 1;
}

// 对外接口:检查整棵树是否平衡
template<class K, class V>
bool AVLTree<K, V>::IsBalance()
{
    _IsBalance(_root);
    return true;
}

1. 函数作用

_IsBalance(cur) 输入 :一个节点 输出 :以这个节点为根的子树高度 副作用:发现不平衡 / BST 错误直接打印提示

2. 执行流程(递归)

  1. 走到空节点 → 返回高度 0
  2. 先算左子树高度右子树高度
  3. 检查 cur->_bf 是否等于 右高 - 左高
  4. 检查 |BF| ≤ 1
  5. 检查是否满足 左 < 根 < 右
  6. 返回当前树高度:max(左,右) + 1

3. 为什么这么写?

  • 递归后序遍历:先算孩子,再算父亲,才能拿到高度
  • 一边算高度,一边检查平衡,一次遍历完成所有检查
  • 能同时检查 平衡因子错误、失衡、BST 错误

1.3.10 AVL树的删除

由于AVL树的删除不常考感兴趣的同学可以自行了解,它也不算是很难,但就是很麻烦

相关推荐
智者知已应修善业8 小时前
【51单片机第5和6位数码管显示0-99自动计数】2023-11-29
c++·经验分享·笔记·算法·51单片机
晚烛8 小时前
CANN 大模型推理优化实战:FlashAttention、推测解码与连续批处理的工程实现
开发语言·人工智能·python·深度学习·数据挖掘
sycmancia8 小时前
Qt——发送自定义事件(下)
开发语言·qt
*愿风载尘*8 小时前
Python多重继承MRO报错问题处理
开发语言·python
图码8 小时前
[特殊字符] 高效统计排序数组中目标元素的出现次数
数据结构·算法·排序算法·柔性数组·图搜索
yqcoder8 小时前
数据的“洁癖”管家:深入解析 JavaScript Set
开发语言·javascript·ecmascript
码界筑梦坊8 小时前
144-基于Flask的电商超市数据可视化分析系统
开发语言·python·信息可视化·数据分析·flask
蜡笔小马8 小时前
14.C++设计模式-状态模式
c++·设计模式·状态模式
之歆8 小时前
Day16_JavaScript Event 对象深度解析(上篇)
开发语言·javascript·ecmascript