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

【C++之容器篇】AVL树的底层原理和使用

目录

    • 前言
    • 一、AVL树
    • 二、AVL树的底层实现
      • 1. 结点类型的定义
      • 2. AVL树的定义
      • 3. 查找函数
      • 4. 插入函数(重难点)
    • 三、判断平衡树的方法

前言

AVL树其实是在搜索树的基础上加上一些限制因素,从而使搜索树的结构保持相对平衡,通过前面我们对二叉搜索树的学习,我们知道,如果我们向一棵二叉搜索树中插入一些有序的数据,那么整棵树就会偏向于一边,从而使整棵树失去平衡,那么在查找的时候就会效率低下,退化为顺序表的效率了,今天我们学习的AVL树本质上就是为了解决二叉搜索树中失去平衡的问题,为了控制二叉搜索树中的子树的左右子树的高度差保持相对平衡,我们采取在树中的每一个结点中增加一个变量来记录每一个结点的平衡因子,我们可以定义平衡因子为该子树的右子树的高度-左子树的高度。

一、AVL树

AVL树是一棵二叉搜索树,并且除了具备二叉搜索树的性质,还具备以下的性质:

  1. 树的左右子树的高度差不超过1,可以通过平衡因子进行控制
  2. 树中的每一棵子树都是AVL树

二、AVL树的底层实现

1. 结点类型的定义

// AVL树的结点
template <class K,class V>
struct AVLTreeNode
{// 构造函数AVLTreeNode(const pair<K,V>& kv):_kv(kv),_left(nullptr),_right(nullptr),_parent(nullptr),_bf(0){}// 成员变量pair<K, V> _kv;AVLTreeNode<K, V>* _left;AVLTreeNode<K, V>* _right;AVLTreeNode<K, V>* _parent;// 平衡因子:左右子树的高度差(右子树高度-左子树高度)int _bf;
};

AVL树中的结点需要包含三个指针,即指向左子树的指针,指向右子树的指针,指向父亲结点的指针,还需要一个存储数据的pair结构,还需要有一个平衡因子,其中的构造函数是对一个新生成的结点中的数据进行初始化,新生成的结点中就是通过给定的kv来初始化_kv,然后左子树和右子树和父亲结点均为空,需要确定后再由外面手动更新,其中的平衡因子是由该结点的左右子树的情况来决定的,显然一个新的结点中不存在左右子树,所以平衡因子显然为0。

2. AVL树的定义

template <class K,class V>
class AVLTree
{// 使用结点typedef AVLTreeNode<K, V> Node;
public:// 成员函数private:// 成员变量Node* _root = nullptr;
};

和前面学习的树型结构的实现一样,刚开始默认为空树,所以_root给了一个缺省值,在AVL树的类中需要使用到树的结点,所以为了方便表示,可以将树的结点进行重定义。

3. 查找函数

查找函数的实现比较简单,和二叉搜索树中的查找函数的实现几乎一致,只是需要知道,这里的数据是pair类型,pair类型中有first和second,其中first是key,second是value,外面需要通过key去进行查找,也就是需要通过kv.first去进行比较。

	// 查找函数bool Find(const pair<K, V>& kv){Node* cur = _root;while (cur){if (kv.first < cur->_kv.first){// 左子树找cur = cur->_left;}else if(kv.first>cur->_kv.first){// 右子树找cur = cur->_right;}else{// 找到了return true;}}return false;}

4. 插入函数(重难点)

AVL树的插入函数的底层其实首先是按照搜索树的规则进行插入,然后再通过平衡因子的调整使整棵树的高度保持相对平衡。

  • 插入的过程
  1. 按照搜索树的规则进行插入
  2. 更新平衡因子,最坏的情况需要沿着路径一直更新到根:如果孩子是插入在左边,则父亲的平衡因子–,如果孩子是插入在父亲的右边,则平衡因子++,当父亲的平衡因子更新之后,需要根据父亲更新后的平衡因子确定作何调整,此时需要进行分类讨论
  • 更新后父亲的平衡因子为0:说明更新前父亲的平衡因子是1或者-1,当更新前父亲的平衡因子是1使,说明这棵子树的右子树比较高,孩子插入在左子树上,所以父亲的高度没有发生变化,不需要继续更新,当更新前父亲的平衡因子是-1时,说明父亲的左子树比较高,此时孩子插入在父亲的右子树上,父亲的高度保持不变,所以不需要更新。
  • 更新后父亲的平衡因子是1或者-1:说明更新前父亲的平衡因子是0,如果更新后父亲的平衡因子变成1,说明孩子插入在父亲的右子树上,导致此时父亲的右子树变高,所以需要继续往上更新。如果更新后父亲的平衡因子变成-1,说明孩子插入在父亲的左子树上,导致父亲的左子树变高,所以需要继续往上更新,往上迭代的方式:先更新cur结点,再通过cur结点算其父亲。
  • 更新后父亲的平衡因子是2或者-2,此时树的平衡因子不符合AVL树的规则,需要进行调整(旋转),下面重点介绍四种旋转方式。
  • 左旋:适用于旋转的子树是整体是右边比较高,首先需要先保存一些关键的结点指针,分别是通过parent计算得出的爷爷结点(pparent),右子树(subR),右子树的左孩子(subRL),旋转的过程中需要注意两个细节:parent可能是整棵树中的某一棵子树,也可能是整棵树的根节点subRL不一定存在在将parent->_right指向subRL之后,需要更新subRL->_parent,但是subRL不一定存在,所以此时需要先判断一下如果parent是根节点,则旋转之后需要更新根节点为subR,并且将subR->_parent指向nullptr。如果parent不是根,则此时pparent结点就会发挥作用,需要先判断parent是pparent的左孩子还是右孩子,以确定是让pparent->_left还是pparent->_right托管subR,然后再更新sub->_parent指向pparent。最后还需要更新平衡因子,左旋的过程中动的只是parent和subR的孩子,所以只需要更新这两个结点,并且左旋的结果是使得它们均平衡,所以最终的平衡因子都是0。
  • 右旋:右旋和左旋是非常类似的,首先需要先保存一些关键的结点指针:通过parent计算得出的pparent(parent的父亲),subL(parent的左孩子),subLR(subL的右孩子)。旋转的过程中同样需要注意两个细节:parent可能是整棵树的根节点,也可能只是某一棵子树的根节点subLR可能存在也可能不存在如果subLR不存在,则不需要更新subRL->_parent判断parent如果parent是整棵树的根节点,则需要更新根节点为subL,然后将根节点的父亲指向空指针,如果parent不是根节点,则此时pparent就会起作用,此时判断parentpparent->_left还是pparent->_right,以决定让pparent->_left还是pparent->_right托管subL,然后再更新subL->_parent指向pparent。最后更新平衡因子,和左旋过程类似,该过程中只是动了parentsubL的孩子,所以只需要更新这两个结点,并且最终都是平衡,所以最终的平衡因子都是0。
  • 左右双旋:这个双旋适合于parent的右子树高,cur的左子树高的情况,先保存一些关键的结点:subR(parent->_right),subRL(subR->_left)旋转的过程,首先让cur右旋,使整棵子树保持右边高,然后再让parent进行左旋。双旋之后,subRL会到根的位置,parent到根的左子树,subR到根的右子树(可以通过画图进行分析),最后调整平衡因子,调整平衡因子需要根据旋转前的subRL的平衡因子(保存为bf)的情况进行分类讨论。如果bf == 0,则subRL是新插入的结点,此时parent,subR,subRL的平衡因子都更新成0。 如果bf == -1,则新节点插入在subRL的左子树,旋转过程中这个新节点会分给左边(parent),所以左边的平衡因子会被平衡故最终的平衡因子为:parent->_bf = 0,subR->_bf = 1,subRL->_bf = 0。如果bf == 1,则新结点插入在subR的右子树,旋转过程中,这个新节点会分给右边(subR),所以最终右边的平衡因子会被平衡,故最终的平衡因子为:parent->_bf = -1,subR->_bf = 0,subRL->_bf = 0
  • 右左双旋:这个双旋适合于parent的左子树高,cur的右子树高的情况,先保存一些关键的结点:subL(parent->_left),subLR(parent->_left->_right),旋转过程:先对subL进行左旋,转化为整体的左子树比较高,再对整体进行右旋,使整棵树保持相对平衡。旋转的结果:subLR到根的位置,parent到根的右子树,subL到根的左子树,再通过旋转前subLR的平衡因子(保存为bf)进行分类讨论:如果bf == 0,则说明subLR是新插入的结点,最终的平衡因子:parent->_bf = 0,subL->_bf = 0,subLR->_bf = 0,如果bf == -1,则说明新结点插入在subLR的左边,这个新结点在旋转的过程中会分给左边(subL),所以左边的平衡因子会被平衡,故最终的平衡因子为:parent->_bf = 1,subL->_bf = 0,subLR->_bf = 0,如果bf == 1,说明新插入的结点插入在subLR的右边,这个新节点在旋转的过程中会分给右边(parent),右边的平衡因子会被平衡,所以最终的平衡因子为:parent->_bf = 0,subL->_bf = -1,subLR->_bf = 0

下面给出上述讲解过程中需要用到的操作的代码:

  • 左旋
// 左旋void RotateL(Node* parent){// 保存一些关键的结点指针Node* pparent = parent->_parent;Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if (subRL){subRL->_parent = parent;}subR->_left = parent;parent->_parent = subR;if (parent == _root){// parent是根_root = subR;_root->_parent = nullptr;}else{// parent不是根if (pparent->_left == parent){pparent->_left = subR;}else{pparent->_right = subR;}subR->_parent = pparent;}// 更新平衡因子parent->_bf = 0;subR->_bf = 0;}
  • 右旋
// 右旋void RotateR(Node* parent){// 保存一些关键结点Node* pparent = parent->_parent;Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;if (subLR){subLR->_parent = parent;}subL->_right = parent;parent->_parent = subL;// 判断parent是否为根if (parent == _root){// parent为根_root = subL;_root->_parent = nullptr;}else{// parent不是根if (pparent->_left == parent){pparent->_left = subL;}else{pparent->_right = subL;}subL->_parent = pparent;}// 更新平衡因子parent->_bf = 0;subL->_bf = 0;}
  • 左右双旋
// 先左旋再右旋void RotateLR(Node* parent){// 先保存一些关键信息Node* subL = parent->_left;Node* subLR = subL->_right;// 旋转前先保存subLR的平衡因子int bf = subLR->_bf;// 旋转RotateL(subL);RotateR(parent);// 更新平衡因子if (bf == 0){// subLR就算新插入的结点parent->_bf = 0;subLR->_bf = 0;subL->_bf = 0;}else if (bf == -1){// 新插入的结点插入在subLR的左边parent->_bf = 1;subLR->_bf = 0;subL->_bf = 0;}else if (bf == 1){// 新插入的结点插入在subLR的右边parent->_bf = 0;subL->_bf = -1;subLR->_bf = 0;}else{// 其他错误assert(false);}}
  • 右左双旋
// 先右旋再左旋void RotateRL(Node* parent){// 保存一些关键信息Node* subR = parent->_right;Node* subRL = subR->_left;// 保存subRL的平衡因子int bf = subRL->_bf;// 旋转RotateR(subR);RotateL(parent);// 更新平衡因子if (bf == 0){// subRL是新插入的结点parent->_bf = 0;subR->_bf = 0;subRL->_bf = 0;}else if (bf == -1){// 新插入的结点插入在subRL的左边parent->_bf = 0;subR->_bf = 1;subRL->_bf = 0;}else if (bf == 1){// 新插入的结点插入在subRL的右边parent->_bf = -1;subR->_bf = 0;subRL->_bf = 0;}else{// 其他错误assert(false);}}

三、判断平衡树的方法

思路:采用递归的方法,先判断当前树是否为平衡树(本节采用平衡因子进行判断),如果当前为平衡树,则继续递归判断左子树和右子树是否为平衡树

  • 代码:
// 求树的高度的子函数size_t _TreeHeight(Node* root){if (root == nullptr){// 空树return 0;}// 非空树// 先算左子树的高度size_t LeftHeight = _TreeHeight(root->_left);// 再算右子树的高度size_t RightHeight = _TreeHeight(root->_right);return LeftHeight > RightHeight ? LeftHeight + 1 : RightHeight + 1;}// 判断平衡树的子函数bool _IsBalanceTree(Node* root){if (root == nullptr){// 空树return true;}// 先算左子树高度size_t LeftHeight = _TreeHeight(root->_left);// 再算右子树高度size_t RightHeight = _TreeHeight(root->_right);// 计算平衡因子int bf = RightHeight - LeftHeight;if (abs(bf) > 1){cout << "树失衡,不断平衡树" << endl;return false;}if (bf != root->_bf){cout << "平衡因子于实际不符合,不是平衡树" << endl;return false;}// 递归判断左右子树return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);}// 判断平衡树bool IsBalanceTree(){return _IsBalanceTree(_root);}

相关文章:

【C++之容器篇】AVL树的底层原理和使用

目录前言一、AVL树二、AVL树的底层实现1. 结点类型的定义2. AVL树的定义3. 查找函数4. 插入函数(重难点)三、判断平衡树的方法前言 AVL树其实是在搜索树的基础上加上一些限制因素&#xff0c;从而使搜索树的结构保持相对平衡&#xff0c;通过前面我们对二叉搜索树的学习&#x…...

从交换机安全配置看常见局域网攻击

前言 构建零信任网络&#xff0c;自然离不开网络准入(NAC)&#xff0c;这就涉及到交换机的一些安全测试&#xff0c;于是有了此文《从交换机安全配置看常见局域网攻击》。 交换机安全配置 如本文标题所说从交换机安全配置看常见的局域网攻击&#xff0c;那么下面提到的各种攻…...

工具篇3.5世界热力图

一、定义 世界热力图是一种地图形式&#xff0c;它使用颜色的变化来显示世界各个地区的某种指标&#xff08;如 GDP、人口、气候等&#xff09;的分布和密度。通常&#xff0c;世界热力图会使用不同的颜色来表示数据的变化&#xff0c;例如使用蓝色表示低值&#xff0c;红色表…...

2023-02-20 leetcode-insertionSortList

摘要: 记录leetcode-insertionSortList的反思 要求: https://leetcode.cn/problems/insertion-sort-list/ Given the head of a singly linked list, sort the list using insertion sort, and return the sorted lists head. The steps of the insertion sort algorithm: In…...

LeetCode初级算法题:环形链表+排列硬币+合并两个有序数组java解法

环形链表排列硬币合并两个有序数组&#xff08;没错&#xff0c;出现过一次的LeetCode合并数组又双叒叕出现了&#xff01;&#xff09;经典算法题java解法 目录1 环形链表题目描述解题思路与代码解法一&#xff1a;哈希表解法二&#xff1a;双指针2 排列硬币题目描述解题思路与…...

网络编程学习一

1、初识网络编程2、网络编程三要素3、三要素&#xff08;IP&#xff09;4、IPV4的一些小细节5、Inetaddress类的使用package com.leitao.demo.network;import java.net.InetAddress; import java.net.UnknownHostException;/*** Description: TODO* Author LeiTao* Date 2023/2…...

Javascript 立即执行函数

IIFE,一般称为立即执行函数。你可能会问我&#xff0c;*“嘿&#xff01;我知道正常的函数表达式是什么样子的&#xff0c;但是 IIFE 到底是什么&#xff1f;”。*好吧&#xff0c;这正是我今天要在本文中回答的问题。 函数表达式 在了解立即调用函数表达式之前&#xff0c;让…...

基于Django和vue的微博用户情感分析系统

完整代码&#xff1a;https://download.csdn.net/download/weixin_55771290/87471350概述这里简单说明一下项目下下来直接跑起的方法。前提先搞好python环境和vue环境,保证你有一个账户密码连上数据库mysql。1、pip install requirements.txt 安装python包2、修改mysql数据库的…...

【C++】IO流

&#x1f308;欢迎来到C专栏~~IO流 (꒪ꇴ꒪(꒪ꇴ꒪ )&#x1f423;,我是Scort目前状态&#xff1a;大三非科班啃C中&#x1f30d;博客主页&#xff1a;张小姐的猫~江湖背景快上车&#x1f698;&#xff0c;握好方向盘跟我有一起打天下嘞&#xff01;送给自己的一句鸡汤&#x1…...

【论文速递】ACL 2021-CLEVE: 事件抽取的对比预训练

【论文速递】ACL 2021-CLEVE: 事件抽取的对比预训练 【论文原文】&#xff1a;CLEVE: Contrastive Pre-training for Event Extraction 【作者信息】&#xff1a;Wang, Ziqi and Wang, Xiaozhi and Han, Xu and Lin, Yankai and Hou, Lei and Liu, Zhiyuan and Li, Peng and …...

《自动驾驶规划入门》专栏结语

一、 源起 2021年10月12日&#xff0c;化学工业出版社的金编辑根据博客中留下的微信号联系上我&#xff0c;问我有没有出书的想法。从小到大&#xff0c;书与文字在我心里是有着神圣地位的。我在“想试试”与“害怕做不好”这两种矛盾的心情中&#xff0c;还是先应了下来。签了…...

【数据结构与算法】2.八大经典排序

文章目录简介1.分析排序算法2.插入排序2.1.直接插入排序2.2.希尔排序3.选择排序3.1.直接选择排序3.2.堆排序3.2.1.堆的数据结构3.2.2.算法实现4.交换排序4.1.冒泡排序4.2.快速排序5.归并排序6.基数排序7.八大排序算法总结简介 排序对于任何一个程序员来说&#xff0c;可能都不会…...

Windows 免安装版mysql,快速配置教程

简单步骤 下载并解压mysql压缩包&#xff0c;把 “<mysql根目录>/bin” 路径添加到系统环境变量path中命令行执行 mysqld --initialize --console&#xff0c;初始化data目录&#xff08;数据库表文件默认存放在" <mysql安装根目录>/data "目录下&#…...

空间误差分析:统一的应用导向处理(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5;&#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密…...

【C++】引用、内联函数、auto关键字、范围for、nullptr

引用什么叫引用引用的特性常引用使用场景传值、传引用效率比较引用和指针的区别内联函数auto关键字(C11)基于范围的for循环(C11)指针空值nullptr(C11)引用 什么叫引用 引用不是新定义一个变量&#xff0c;而是给已存在变量取了一个别名&#xff0c;编译器不会为引用变量开辟内…...

pytest数据驱动

文章目录一、数据驱动概念二、数据驱动yaml1、yaml的基本语法&#xff1a;2、yaml支持的数据格式&#xff1a;3、安装4、使用5、读取方法a、目录结构b、yaml文件c、测试方法d、测试用例e、测试结果三、数据驱动excel1、安装导入2、操作3、读取方法a、目录结构b、excel文件c、测…...

OSI七层网络模型

应用层 定义了各种应用协议规范数据格式&#xff1a;HTTP协议、HTTPS协议、FTP协议、DNS协议、TFTP、SMTP等等。 表示层 翻译工作。提供一种公共语言、通信。 会话层 1、可以从校验点继续恢复数据进行重传。——大文件 2、自动收发&#xff0c;自动寻址的功能。 传输层 1、…...

易基因|MeRIP-seq揭示m6A RNA甲基化通过调控组蛋白泛素化来促进癌症生长和进展:Cancer Res

大家好&#xff0c;这里是专注表观组学十余年&#xff0c;领跑多组学科研服务的易基因。2022年05月16日&#xff0c;《Cancer Res》杂志发表了题为“M6A RNA Methylation Regulates Histone Ubiquitination to Support Cancer Growth and Progression”的研究论文&#xff0c;该…...

Java 日期处理踩过的坑

前言 整理Java日期处理遇到过的问题&#xff0c;希望对大家有帮助 制作不易&#xff0c;一键三连&#xff0c;谢谢大家。 1.用 Calendar 设置时间的坑 反例&#xff1a; //提供者模式获取实例Calendar calendar Calendar.getInstance();//获取当前时间Date currentDate c…...

一文吃透 Spring 中的IOC和DI(二)

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…...

【大模型RAG】Docker 一键部署 Milvus 完整攻略

本文概要 Milvus 2.5 Stand-alone 版可通过 Docker 在几分钟内完成安装&#xff1b;只需暴露 19530&#xff08;gRPC&#xff09;与 9091&#xff08;HTTP/WebUI&#xff09;两个端口&#xff0c;即可让本地电脑通过 PyMilvus 或浏览器访问远程 Linux 服务器上的 Milvus。下面…...

[10-3]软件I2C读写MPU6050 江协科技学习笔记(16个知识点)

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16...

让AI看见世界:MCP协议与服务器的工作原理

让AI看见世界&#xff1a;MCP协议与服务器的工作原理 MCP&#xff08;Model Context Protocol&#xff09;是一种创新的通信协议&#xff0c;旨在让大型语言模型能够安全、高效地与外部资源进行交互。在AI技术快速发展的今天&#xff0c;MCP正成为连接AI与现实世界的重要桥梁。…...

力扣-35.搜索插入位置

题目描述 给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并返回其索引。如果目标值不存在于数组中&#xff0c;返回它将会被按顺序插入的位置。 请必须使用时间复杂度为 O(log n) 的算法。 class Solution {public int searchInsert(int[] nums, …...

React---day11

14.4 react-redux第三方库 提供connect、thunk之类的函数 以获取一个banner数据为例子 store&#xff1a; 我们在使用异步的时候理应是要使用中间件的&#xff0c;但是configureStore 已经自动集成了 redux-thunk&#xff0c;注意action里面要返回函数 import { configureS…...

JS设计模式(4):观察者模式

JS设计模式(4):观察者模式 一、引入 在开发中&#xff0c;我们经常会遇到这样的场景&#xff1a;一个对象的状态变化需要自动通知其他对象&#xff0c;比如&#xff1a; 电商平台中&#xff0c;商品库存变化时需要通知所有订阅该商品的用户&#xff1b;新闻网站中&#xff0…...

Caliper 配置文件解析:fisco-bcos.json

config.yaml 文件 config.yaml 是 Caliper 的主配置文件,通常包含以下内容: test:name: fisco-bcos-test # 测试名称description: Performance test of FISCO-BCOS # 测试描述workers:type: local # 工作进程类型number: 5 # 工作进程数量monitor:type: - docker- pro…...

windows系统MySQL安装文档

概览&#xff1a;本文讨论了MySQL的安装、使用过程中涉及的解压、配置、初始化、注册服务、启动、修改密码、登录、退出以及卸载等相关内容&#xff0c;为学习者提供全面的操作指导。关键要点包括&#xff1a; 解压 &#xff1a;下载完成后解压压缩包&#xff0c;得到MySQL 8.…...

深入浅出Diffusion模型:从原理到实践的全方位教程

I. 引言&#xff1a;生成式AI的黎明 – Diffusion模型是什么&#xff1f; 近年来&#xff0c;生成式人工智能&#xff08;Generative AI&#xff09;领域取得了爆炸性的进展&#xff0c;模型能够根据简单的文本提示创作出逼真的图像、连贯的文本&#xff0c;乃至更多令人惊叹的…...

tauri项目,如何在rust端读取电脑环境变量

如果想在前端通过调用来获取环境变量的值&#xff0c;可以通过标准的依赖&#xff1a; std::env::var(name).ok() 想在前端通过调用来获取&#xff0c;可以写一个command函数&#xff1a; #[tauri::command] pub fn get_env_var(name: String) -> Result<String, Stri…...