当前位置: 首页 > news >正文

深入学习c++之---AVL树

VL树简介

AVL树是一种自平衡二叉搜索树,通过平衡因子(Balance Factor, BF)​旋转操作,确保树始终保持平衡,避免退化成链表,从而保证查找、插入、删除的时间复杂度稳定在 ​O(log n)​

核心特点
  1. 平衡条件​:每个节点的左右子树高度差不超过1(BF ∈ {-1, 0, 1})。
  2. 调整方法​:通过四种旋转​(右单旋、左单旋、左右双旋、右左双旋)修复失衡。
  3. 优势​:比普通BST更稳定,适合频繁动态更新的场景。

目录

VL树简介​

​核心特点​

一, 搜索二叉树的固有问题:

二,平衡因子:

1`基本定义:

2`补充说明:

三,结构的设计:

        1`扩充的成员变量: 

         2`小坑: 

四,元素的插入:

         1`插入元素 :

         2`平衡因子的维护:

        3`旋转:

        1`右单旋:

        2`左单旋:

        3`左右双旋:

        4`右左双旋:

五`元素的查找(按值):

 六`中序遍历顺序输出:

七`类的结构和成员函数代码汇总:


一, 搜索二叉树的固有问题:

  •         理想情况下 , 搜索二叉树的搜索效率已经足够高 , 仅仅log(N)的时间复杂度 .
  •         但是 , 如果是按照近似有序序列来插入 , 会导致树形结构退化为普通的链式结构 (没错,和普通单链表一样了!!!).
  •         于是 , 本节索要探讨的AVL树的结构就应运而生了(AVL是按照两个前苏联科学家的名字来命名的,没有特殊含义).

二,平衡因子:

        想要避免搜索二叉树的退化 , 就需要在恰当的时候通过旋转来调整(之后的内容) , 而为了能够更加优雅地理清旋转的逻辑和简化相关代码编写 , 平衡因子是我们可以提前准备的小工具 .

1`基本定义:

  • 节点初始的平衡因子为0
  • 在节点左边插入新节点 , 平衡因子 +1
  • 在节点右边插入新节点,平衡因子 -1
  • 形象的来说 , 一个节点的平衡因子 = 右子树高度 - 左子树高度 .

2`补充说明:

  •         平衡因子可以让我们以量化的方式来判断一个节点左右子树的高度差 , 以此来决定是否需要旋转.
  •          对于AVL树来说 , 任何一个节点的平衡因子的绝对值必须小于等于1 .
  •          之所以没有要求节点的平衡因子严格等于0 , 是因为在部分情况下不现实 , 比如整棵树只有两个节点时 , 那要么左子树高度低要么右子树高度低 .
  •          下面列举几个例子来具象化展示: 
平衡因子为0的根节点

平衡因子的绝对值为1的根节点, 此处为-1


平衡因子的绝对值大于等于2的节点 , 此处为-2
平衡因子做不到为0的情况

三,结构的设计:

     总的类框架没变 , 还是分为一个主类来包含一个根节点的成员变量以及各种成员函数 , 和一个节点类 . 只是节点类的成员变量稍有扩充   

        1`扩充的成员变量: 

AVL树
和搜索二叉树相同的成员变量值key / 左孩子left / 右孩子right
新增的成员变量一 : 平衡因子BF (单词 balance factor的缩写)
新增的成员变量二:父节点parent (方便检索平衡因子)
//节点类
template<class K , class V>
struct AvlNode
{//构造AvlNode(K key , V val){_k.first = key;_k.second = val;_parent = nullptr;_left = nullptr;_right = nullptr;}//成员变量pair<K, V> _k;AvlNode* _parent;  //新增AvlNode* _left;AvlNode* _right;int _bf = 0;    //新增
};

         2`小坑: 

        对于一颗初始为空的AVL树 , 成员变量(_root)应该默认初始化为nullptr , 否则难以应对向空树中插入元素时的情况.

//AVL树的类声明
template<class K>
class AVLTree
{
public:using Node = AVLNode<K>;
private:Node* _root == nullptr;   //c++11的语法,用于初始化列表的缺省值
};

四,元素的插入:

        AVL树就是在搜索二叉树的基础上通过旋转来维持平衡 , 而插入元素的函数里也包含了平衡因子的维护和旋转的逻辑.

         1`插入元素 :

        AVL树插入元素的逻辑和二叉搜索树类似 , 只不过每个节点多出来的BF(平衡因子)和parent(父节点)要求略微的调整.

  1. 函数接受一个插入的值 key.
  2. 定义一个指向根节点的局部指针变量cur , 和一个备用的局部指针变量parent来保存cur每次更新之前的值(放标最后链接)
  3. 每次通过判断cur节点的key和待插入的key的大小来决定往左子树还是右子树更新.
  4. 如果cur走到空,则找到插入位置,借助以保存的parent变量来连接新节点 ; 如果cur走到值和key相同的节点,说明已存在该值,不做处理直接结束.

特殊情况 : 如果起初的根节点为空(nullptr) , 则直接让新节点作根,结束函数体.

//insert函数插入数据的部分
//-----------------------------------------------------------
//根为空的空树情况
if (nullptr == _root)
{_root = new Node(key);return true;
}Node* parent = nullptr;
Node* cur = _root;
while (cur)
{if (key < cur->_key) //待插入的节点值小于当前节点,则往左子树{parent = cur;cur = cur->_left}else if (key > cur->_key) //待插入的节点值大于当前节点,则往右子树{parent = cur;cur = cur->_right;}else{return false;}
}
Node* newNode = new Node(key);
//判断新节点应该连接到父节点的哪边(通过值判断!!!)
if (key < parent->_key)
{parent->_left = newNode;
}
else
{parent->_right = newNode;
}
newNode->_parent = parent;

         2`平衡因子的维护:

        前面已经基本介绍过了节点平衡因子的定义 , 用代码维护起来也比较简单.

  1. 使用两个局部指针变量 , 一个新插入节点的指针cur , 另一个是他的父节点指针parent
  2. 新节点cur的平衡因子是天然的0 , 所以从parent的平衡因子开始判断.
  3. 若新节点cur是parent的左节点 , parent的平衡因子BS减一 ;                                         若新节点cur是parent的右节点 , parent的平衡因子BS加一;
  4. 接下来直接判断更新后的当前parent的平衡因子 , 有三种情况:                                                  第一种 : parent更新后平衡因子为0 , 说明平衡了 , 不用旋转 , 结束插入的过程.              第二种 : parent更新后的平衡因子为 1或-1 , 说明还是平衡 , 但是需要将cur和parent全部往上更新一层 , 进行第 3 点的操作.                                                                            第三种 : parent更新后的平衡因子为2或-2 , 说明当前节点的左右子树不平衡 , 需要旋转(下文的内容,此处省略).
//维护平衡因子
cur = newNode;
while (parent)
{if (parent->_left == cur){parent->_bf--;}else{parent->_bf++;}if (parent->_bf == 0){return true;}else if (abs(parent->_bf) == 1){cur = parent;parent = parent->_parent;}else if (abs(parent->_bf) == 2){//旋转的类型和逻辑 , 此处略}else{assert(false); //防御性编程}
}

        3`旋转:

        旋转是最有意思的部分 , 通过节点不平衡的情况来判断旋转的类型 .

        旋转的类型有左单旋/右单旋/右左双旋/左右双旋.

  1. 单旋转 : 存粹的左子树高或者右子树高 , 可以大致理解为新插入的节点是左子树的最左边或右子树的最右边 .
  2. 双旋转:不存粹的左子树高或者右子树高 , 可以大致理解为新插入的节点是左子树的叶子结点的右侧 或 右子树的叶子结点的左侧.

        1`右单旋:

// 右单旋
void RotateR(Node* root) 
{Node* subL = root->_left;Node* subLR = subL->_right;//将左子树的左子树节点链接给旋转节点的左边root->_left = subLR;if (subLR != nullptr) //防止左子树的左子树的跟节点为空{subLR->_parent = root;}//提前保存节点旋转之前的父节点Node* oldParent = root->_parent;//将旋转节点和其左子节点颠倒父子关系subL->_right = root;root->_parent = subL;if (root == _root) //待旋转节点为整棵树的跟的情况{_root = subL;subL->_parent = nullptr;}else{//让subL和root的父节点建立链接if (oldParent->_right == root){oldParent->_right = subL;}else{oldParent->_left = subL;}subL->_parent = oldParent;}//更新平衡因子root->_bf = 0;subL->_bf = 0;
}

 

        2`左单旋:

//左单旋
void RotateL(Node* root)
{Node* subR = root->_right;Node* subRL = subR->_left;root->_right = subRL;if (subRL != nullptr){subRL->_parent = root;}Node* oldParent = root->_parent;subR->_left = root;root->_parent = subR;if (root == _root){_root = subR ;subR->_parent = nullptr;}else{if (oldParent->_left == root){oldParent->_left = subR;}else{oldParent->_right = subR;}subR->_parent = oldParent;}root->_bf = 0;subR->_bf = 0;
}

 

        3`左右双旋:

//左右双旋
void RotateLR(Node* root)
{Node* subL = root->_left;Node* subLR = subL->_right;int bf_subLR = subLR->_bf;RotateL(subL);RotateR(root);if (bf_subLR == 0){root->_bf = 0;subL->_bf = 0;subLR->_bf = 0;}else if (bf_subLR == 1){root->_bf = 0;subL->_bf = -1;subLR->_bf = 0;}else if (bf_subLR == -1){root->_bf = 1;subL->_bf = 0;subLR->_bf = 0;}else{assert(false);}
}

        4`右左双旋:

//右左双旋
void RotateRL(Node* root)
{Node* subR = root->_right;Node* subRL = subR->_left;int bf_subRL = subRL->_bf;RotateR(subR);RotateL(root);if (bf_subRL == 0){root->_bf = 0;subR->_bf = 0;subRL->_bf = 0;}else if(bf_subRL == 1){root->_bf = -1;subR->_bf = 0;subRL->_bf = 0;}else if (bf_subRL == -1){root->_bf = 1;subR->_bf = 0;subRL->_bf = 0;}else{assert(false);}
}

五`元素的查找(按值):

        元素的查找平平无奇 , 和搜索二叉树一个样 ,  都是拿着待查找的值key从根节点开始比对 , key更小则找左子树,更大则找右子树 .

//按值查找节点
Node* Find(const K& key)
{Node* cur = _root;while (cur){if (key < cur->_key){cur = cur->_left;}else if (key > cur->_key){cur = cur->_right;}else{return cur;}}return false;
}

 六`中序遍历顺序输出:

        一般来说 , AVL树的涉及使得  左子树的值<根节点的值<右子树的值 . 所以通过中序遍历(左子树->根节点->左子树)正好就可以得到一个顺序序列.

        需要注意的是:树形结构的递归操作需要将根节点作为参数传递,但由于根节点通常作为类的私有成员变量,无法在外部直接访问。因此,在C++中常见的做法是提供一个公有成员函数来传递根节点,从而确保内部的递归函数能够正常运行。

//设置为共有成员函数,方便外部调用
public:
void MidTrave()
{_MidTrave(_root);cout << endl;
}//设置成私有成员函数,仅仅让类类内部访问
private:
void _MidTrave(Node* root)
{if (nullptr == root){return;}_MidTrave(root->_left);cout << root->_key << " ";_MidTrave(root->_right);
}

七`类的结构和成员函数代码汇总:

....//头文件的包含此处省略
----------------------------
//节点类的声明
template<class K>
struct AVLNode
{AVLNode(K key):_key(key), _parent(nullptr), _left(nullptr), _right(nullptr), _bf(0){}K _key;AVLNode<K>* _parent;AVLNode<K>* _left;AVLNode<K>* _right;int _bf;
};//AVL树的类声明
template<class K>
class AVLTree
{
public:using Node = AVLNode<K>;//插入bool Insert(const K& key){//根为空的空树情况if (nullptr == _root){_root = new Node(key);return true;}Node* parent = nullptr;Node* cur = _root;while (cur){if (key < cur->_key) //待插入的节点值小于当前节点,则往左子树{parent = cur;cur = cur->_left;}else if (key > cur->_key) //待插入的节点值大于当前节点,则往右子树{parent = cur;cur = cur->_right;}else{return false;}}Node* newNode = new Node(key);//判断新节点应该连接到父节点的哪边(通过值判断!!!)if (key < parent->_key){parent->_left = newNode;}else{parent->_right = newNode;}newNode->_parent = parent;//----------------------------------------------------------------//维护平衡因子cur = newNode;while (parent){if (parent->_left == cur){parent->_bf--;}else{parent->_bf++;}if (parent->_bf == 0){return true;}else if (abs(parent->_bf) == 1){cur = parent;parent = parent->_parent;}else if (abs(parent->_bf) == 2){if (parent->_bf == -2 && cur->_bf <= 0) //右单旋{RotateR(parent);}else if (parent->_bf == 2 && cur->_bf >= 0) //左单旋{RotateL(parent);}else if (parent->_bf == -2 && cur->_bf >= 0) //左右双旋{RotateLR(parent);}else if (parent->_bf == 2 && cur->_bf <= 0) //右左双旋{RotateRL(parent);}else{assert(false);}break;}else{assert(false);}}return true;}// 右单旋void RotateR(Node* root) {Node* subL = root->_left;Node* subLR = subL->_right;//将左子树的左子树节点链接给旋转节点的左边root->_left = subLR;if (subLR != nullptr) //防止左子树的左子树的跟节点为空{subLR->_parent = root;}//提前保存节点旋转之前的父节点Node* oldParent = root->_parent;//将旋转节点和其左子节点颠倒父子关系subL->_right = root;root->_parent = subL;if (root == _root) //待旋转节点为整棵树的跟的情况{_root = subL;subL->_parent = nullptr;}else{//让subL和root的父节点建立链接if (oldParent->_right == root){oldParent->_right = subL;}else{oldParent->_left = subL;}subL->_parent = oldParent;}//更新平衡因子root->_bf = 0;subL->_bf = 0;}//左单旋void RotateL(Node* root){Node* subR = root->_right;Node* subRL = subR->_left;root->_right = subRL;if (subRL != nullptr){subRL->_parent = root;}Node* oldParent = root->_parent;subR->_left = root;root->_parent = subR;if (root == _root){_root = subR ;subR->_parent = nullptr;}else{if (oldParent->_left == root){oldParent->_left = subR;}else{oldParent->_right = subR;}subR->_parent = oldParent;}root->_bf = 0;subR->_bf = 0;}//左右双旋void RotateLR(Node* root){Node* subL = root->_left;Node* subLR = subL->_right;int bf_subLR = subLR->_bf;RotateL(subL);RotateR(root);if (bf_subLR == 0){root->_bf = 0;subL->_bf = 0;subLR->_bf = 0;}else if (bf_subLR == 1){root->_bf = 0;subL->_bf = -1;subLR->_bf = 0;}else if (bf_subLR == -1){root->_bf = 1;subL->_bf = 0;subLR->_bf = 0;}else{assert(false);}}//右左双旋void RotateRL(Node* root){Node* subR = root->_right;Node* subRL = subR->_left;int bf_subRL = subRL->_bf;RotateR(subR);RotateL(root);if (bf_subRL == 0){root->_bf = 0;subR->_bf = 0;subRL->_bf = 0;}else if(bf_subRL == 1){root->_bf = -1;subR->_bf = 0;subRL->_bf = 0;}else if (bf_subRL == -1){root->_bf = 1;subR->_bf = 0;subRL->_bf = 0;}else{assert(false);}}//按值查找节点Node* Find(const K& key){Node* cur = _root;while (cur){if (key < cur->_key){cur = cur->_left;}else if (key > cur->_key){cur = cur->_right;}else{return cur;}}return nullptr;}void MidTrave(){_MidTrave(_root);cout << endl;}
private:void _MidTrave(Node* root){if (nullptr == root){return;}_MidTrave(root->_left);cout << root->_key << " ";_MidTrave(root->_right);}Node* _root = nullptr;
};

http://www.dtcms.com/a/267527.html

相关文章:

  • 支持零样本和少样本的文本到语音48k star的配音工具:GPT-SoVITS-WebUI
  • 完成ssl不安全警告
  • DQL-6-分页查询
  • Redis的编译安装
  • PVE DDNS IPV6
  • 超详细yolo8/11-detect目标检测全流程概述:配置环境、数据标注、训练、验证/预测、onnx部署(c++/python)详解
  • Altium Designer使用教程 第一章(Altium Designer工程与窗口)
  • ESXi 8.0 SATA硬盘直通
  • python-字符串
  • 量化可复用的UI评审标准(试验稿)
  • OPENPPP2 VDNS 核心域模块深度解析
  • 电源管理芯片(PMIC) 和 电池管理芯片(BMIC)又是什么?ING
  • webpack+vite前端构建工具 -11实战中的配置技巧
  • 合肥工会入会的注意事项和常见问答
  • springBoot接口层时间参数JSON序列化问题,兼容处理
  • Modbus_TCP_V4 客户端
  • Day52
  • 人工智能-基础篇-18-什么是RAG(检索增强生成:知识库+向量化技术+大语言模型LLM整合的技术框架)
  • ES6-in 的用法
  • Apollo自动驾驶系统中Planning(路径规划)模块的架构设计和核心逻辑
  • leetcode86.分隔链表
  • 1. 两数之和 (leetcode)
  • 【网络】Linux 内核优化实战 - net.ipv4.tcp_timestamps
  • 【Docker基础】Docker数据卷管理:docker volume prune及其参数详解
  • CSS 文字浮雕效果:巧用 text-shadow 实现 3D 立体文字
  • 一体化步进伺服电机在无人机扫地机器人中的应用案例
  • 隐马尔可夫模型:语音识别系统的时序解码引擎
  • 写传播和写策略
  • 【Linux】常用基本指令
  • 量化交易中的隐藏模式识别:基于潜在高斯混合模型的机会挖掘