C++笔记:从零开始一步步手撕高阶数据结构AVL树

文章目录

  • 高度平衡二叉搜索树
  • 实现一颗AVL树
    • 结点与树的描述——定义类
    • AVL树的插入操作
      • 步骤1:按照二叉搜索树的方法插入结点
      • 步骤2:自底向上调整平衡因子
      • 步骤3:触发旋转操作(AVL树平衡的精髓)
        • 右单旋
        • 左单旋
        • 左右双旋
        • 右左双旋
    • 验证AVL树是否平衡
  • 参考文章

高度平衡二叉搜索树

二叉搜索树是一种特殊的树形数据结构,一般情况下,该树能够缩短查找的效率,但是它有个缺陷,在结点的插入或删除顺序较为特殊时结构会退化成链表,导致搜索、插入和删除等操作的时间复杂度从O(log n)退化到O(n)。

【二叉搜索树退化成链表的例子】
在这里插入图片描述

高度平衡二叉搜索树是针对二叉搜索树的缺陷所发明出来的一种改良结构。

高度平衡二叉搜索树常被称为 “ AVL树 ”,这主要是为了纪念发明它两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis,AVL是两位数学家的名字的缩写。

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

  • 左右子树高度之差(简称为平衡因子)的绝对值不超过1。
  • 在这里平衡因子的求法定义为:右子树的高度 - 左子树的高度。
  • 结点的左右两棵子树也都是一棵平衡二叉树。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

实现一颗AVL树

概念部分讲的差不多了,至于AVL树相较于二叉搜索树是如何保持平衡结构,就在接下来的实现过程中一步步讲解。

结点与树的描述——定义类

namespace ljh
{template<class K, class V>struct AVLTreeNode{AVLTreeNode(const pair<K, V> kv) : _kv(kv) {}AVLTreeNode<K, V>* _parent = nullptr;	// A pointer to node's fatherAVLTreeNode<K, V>* _left = nullptr;		// A pointer to node's left childAVLTreeNode<K, V>* _right = nullptr;	// A pointer to node's right childint _bf = 0;		// balance factorpair<K, V> _kv;		// key-value};template<class K, class V>class AVLTree{typedef AVLTreeNode<K, V> Node;public:// AVL树的操作方法protected:Node* _root = nullptr;};
}

【说明】

  1. 模板化设计:
    • 使用template<class K, class V>来定义AVLTreeNodeAVLTree,使得该数据结构能够处理任意类型的键(Key)和值(Value),提高了代码的复用性和灵活性。
    • K代表键的类型,V代表值的类型。使用者可以根据自己的需求,在AVL树存储任何类型的键值对。
  2. AVLTreeNode类:
    • AVLTreeNode结构体定义了AVL树中每个节点的结构,用struct定义是为了方便在树类访问。
    • _parent指针:指向父节点,用于在旋转等操作中快速定位父节点(记住这里的旋转,这将是后面的重点)。
    • _left_right指针:分别指向左孩子和右孩子,是二叉树结构的基础。
    • _bf(平衡因子):存储节点的平衡因子,用于衡量树是否平衡。
    • _kv:存储于结点中的键值对,其中K是键的类型,V是值的类型。
  3. 构造函数:
    • AVLTreeNode的构造函数接收一个pair<K, V>对象,并初始化_kv成员变量。这样,当创建新节点时,可以方便地传入键值对。
  4. AVLTree类:
    • AVLTree类代表整个AVL树结构。
    • typedef AVLTreeNode<K, V> Node;,在内部使用定义了一个类型别名Node,目的是简化代码书写。
    • _root指针:指向AVL树的根节点,是整棵树的入口点。
  5. 保护成员:
    • _root成员变量被设计为protected,意味着它只能在AVLTree类及其派生类中被访问。这种设计是为了将AVL树的内部实现细节隐藏起来,而只暴露必要的公共接口给外部使用。
  6. 命名空间:
    • 为了方便使用库函数,使用using namespace std;展开标准库,但这容易引发同名类或函数发生冲突,因而将所有定义放在ljh命名空间中。

AVL树的插入操作

AVL树的插入操作实现起来大致分成以下三个大步骤

步骤1:按照二叉搜索树的方法插入结点

  1. 树为空,则构造新结点,让_root 指针指向该结点,返回true
  2. 树不空,按key的大小寻找插入位置,如果已存在,按插入失败处理,返回false
  3. 走到空表示找到合适位置,然后插入构造的新结点,插入时要判断左边插入或者右边插入。

此时插入并未结束,接下来进行步骤二的平衡因子更新操作!

【步骤1的代码如下:】

bool insert(pair<K, V> kv)
{// empty tree -> 直接插入if(_root == nullptr){_root = new Node(kv);return true;}// not empty tree -> 找到合适的位置再插入else{Node* child = _root;Node* parent = nullptr;while (child){// 大,往右走if (child->_kv.first < kv.first){parent = child;child = child->_right;}// 小,往左走else if (child->_kv.first > kv.first){parent = child;child = child->_left;}// 相同,插入失败else{return false;}}// child == nullptr, 找到合适的位置child = new Node(kv);if (child->_kv.first > parent->_kv.first)	parent->_right = child;else	parent->_left = child;child->_parent = parent;// 自底向上更新平衡因子// ...}
}

步骤2:自底向上调整平衡因子

我们将新插入结点称为child,新插入结点的双亲结点称为parent

平衡因子的更新规则如下:

  • 如果childparent的左孩子,parent的平衡因子-1。
  • 如果childparent的右孩子,parent的平衡因子+1。

这是第一次更新,更新完之后要不要继续向上更新取决于以parent为根结点的这棵树的高度是否变化,情况有以下3种:

  1. 平衡因子更新后,parent的平衡因子为0
    这意味着parent->_bf从-1 -> 0或者 从1 -> 0,以parent为根结点的这棵树的高度没有发生变化,不用再向上更新平衡因子,可以返回true,表示插入成功。

  2. 平衡因子更新后,parent的平衡因子为-1+1
    这意味着parent->_bf从0 -> -1或者 从0 -> 1,以parent为根结点的这棵树的高度发生了变化,但还没有达到需要旋转的程度。
    在这种情况下,更新child = child->_parent,更新parent = parent->_parent,继续更新parent 的平衡因子,直到情况1:找到平衡因子为0的节点;或者情况2:到达根节点(parent == nullptr)。

  3. 平衡因子更新后,parent的平衡因子为-2+2
    这意味着此时以parent为根结点的这棵树已经违反了AVL树的规则,需要进行旋转处理,处理完之后,可以直接返回true

【步骤2的代码如下:】

// 自底向上更新平衡因子
while (parent)
{if (child == parent->_left){--parent->_bf;}else{++parent->_bf;}if (parent->_bf == 0){return true;}else if (parent->_bf == 1 || parent->_bf == -1){child = child->_parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){// 违反规则,旋转处理// ...}else{// 理论上没错误不会走到这里assert(false);}
}

步骤3:触发旋转操作(AVL树平衡的精髓)

根据节点插入位置的不同,AVL树的旋转分为以下4种:

由于具象图的种类繁多,根本不可能画得完,下面AVL树旋转的图解中大多画的是抽象图。

右单旋

这里给出了一棵以结点90作为根节点的AVL树的抽象图,图中 a / b / c 代表三棵高度为 h 的子树。
这颗AVL树的左子树高度高于右子树高度

在这里插入图片描述


首先,以90为根结点的这棵树有三种可能:

  1. 它是某棵AVL树的左子树;
  2. 它是某棵AVL树的右子树;
  3. 它就是AVL树的根结点;

在这里插入图片描述


当新结点插入在了较高左子树的左侧,即 a 子树时,child 和 parent 自底向上更新平衡因子,当出现parent->_bf == -2, child->_bf == -1时,该树违反了AVL树的规则,需要进行右单旋操作。
在这里插入图片描述


在右单旋中,涉及到改变链接关系的结点主要有以下4个:
在这里插入图片描述


当 a / b / c 3棵子树高度为零时插入新结点,平衡因子为 -2 的结点向右旋转:
在这里插入图片描述

当 a / b / c 3棵子树高度不为零时插入新结点,平衡因子为 -2 的结点向右旋转:
在这里插入图片描述


【右单旋操作的代码如下】

void R_Rotate(Node* parent)
{Node* ppnode = parent->_parent;Node* subL = parent->_left;Node* subLR = subL->_right;// subL and parentsubL->_right = parent;parent->_parent = subL;// parent and subLRparent->_left = subLR;if (subLR)	subLR->_parent = parent;// ppnode and subLif (ppnode == nullptr){_root = subL;subL->_parent = nullptr;}else{subL->_parent = ppnode;if (parent == ppnode->_left){ppnode->_left = subL;}else{ppnode->_right = subL;}}parent->_bf = subL->_bf = 0;
}
左单旋

这里给出了一棵以结点30作为根节点的AVL树的抽象图,图中 a / b / c 代表三棵高度为 h 的子树。
这颗AVL树的右子树高度高于左子树高度

在这里插入图片描述


首先,以30为根结点的这棵树有三种可能:

  1. 它是某棵AVL树的左子树;
  2. 它是某棵AVL树的右子树;
  3. 它就是AVL树的根结点;

在这里插入图片描述


当新结点插入在了较高右子树的右侧,即 c 子树时,child 和 parent 自底向上更新平衡因子,当出现parent->_bf == 2, child->_bf == 1时,该树违反了AVL树的规则,需要进行左单旋操作。
在这里插入图片描述


在左单旋中,涉及到改变链接关系的结点同样有4个:
在这里插入图片描述


当 a / b / c 3棵子树高度为零时插入新结点,平衡因子为 2 的结点向左旋转:
在这里插入图片描述

当 a / b / c 3棵子树高度不为零时插入新结点,平衡因子为 2 的结点向左旋转:
在这里插入图片描述


【左单旋操作的代码如下】

void L_Rotate(Node* parent)
{Node* ppnode = parent->_parent;Node* subR = parent->_right;Node* subRL = subR->_left;// subR and parentsubR->_left = parent;parent->_parent = subR;// parent and subRLparent->_right = subRL;if (subRL)	subRL->_parent = parent;//ppnode and subRif (ppnode == nullptr){_root = subR;subR->_parent = nullptr;}else{subR->_parent = ppnode;if (parent == ppnode->_left){ppnode->_left = subR;}else{ppnode->_right = subR;}}parent->_bf = subR->_bf = 0;
}
左右双旋

这里给出了一棵以结点90作为根节点的AVL树的抽象图,图中 a / b / c / d 代表四棵子树。
这颗AVL树的左子树高度高于右子树高度

在这里插入图片描述


首先,以90为根结点的这棵树有三种可能:

  1. 它是某棵AVL树的左子树;
  2. 它是某棵AVL树的右子树;
  3. 它就是AVL树的根结点;

在这里插入图片描述


下面三种情况都有一个共同给特点,就是parent->_bf == -2, child->_bf == 1
我们不难发现,左右双旋可以视为先左旋再右旋,即结点30先左旋,结点90再右旋。
在这里插入图片描述


在这里插入图片描述


单旋中的subLR不管怎么操作,它的平衡因子都是 0,但是在双旋中,subLR有可能是 -1、0、1中任意一种,因此,虽然双旋操作我们可以复用单旋的代码,但是双旋之后的平衡因子调整需要单独处理。
在这里插入图片描述

【左右双旋的代码如下】

void LR_Rotate(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;// 双旋之后要靠bf来更新平衡因子int bf = subLR->_bf;L_Rotate(subL);R_Rotate(parent);if (bf == 0){subLR->_bf = 0;subL->_bf = 0;parent->_bf = 0;}else if (bf == -1){subLR->_bf = 0;subL->_bf = 0;parent->_bf = 1;}else{subLR->_bf = 0;subL->_bf = -1;parent->_bf = 0;}
}
右左双旋

这里给出了一棵以结点30作为根节点的AVL树的抽象图,图中 a / b / c / d 代表四棵子树。
这颗AVL树的右子树高度高于左子树高度

在这里插入图片描述


首先,以30为根结点的这棵树有三种可能:

  1. 它是某棵AVL树的左子树;
  2. 它是某棵AVL树的右子树;
  3. 它就是AVL树的根结点;

在这里插入图片描述


下面三种情况都有一个共同给特点,就是parent->_bf == 2, child->_bf == -1
我们不难发现,右左双旋可以视为先右旋再左旋,即结点90先右旋,结点30再左旋。
在这里插入图片描述


在这里插入图片描述


单旋中的subRL不管怎么操作,它的平衡因子都是 0,但是在双旋中,subRL有可能是 -1、0、1中任意一种,因此,虽然双旋操作我们可以复用单旋的代码,但是双旋之后的平衡因子调整需要单独处理。
在这里插入图片描述

【右左双旋的代码如下】

void RL_Rotate(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;// 双旋之后要靠bf来更新平衡因子int bf = subRL->_bf;R_Rotate(subR);L_Rotate(parent);if (bf == 0){subRL->_bf = 0;parent->_bf = 0;subR->_bf = 0;}else if (bf == -1){subRL->_bf = 0;parent->_bf = 0;subR->_bf = 1;}else{subRL->_bf = 0;parent->_bf = -1;subR->_bf = 0;}
}

验证AVL树是否平衡

虽然目前已经将AVL树的插入操作的代码已经写出来了,但是仅仅是写出来了一定能够保证代码就是正确的吗——肯定不是!

所以,接下来还要再实现一个方法,来验证一棵AVL树是不是平衡的。

AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:

  1. 验证其为二叉搜索树
    如果中序遍历可得到一个有序的序列,就说明为二叉搜索树。
  2. 验证其为平衡树
    每个结点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)。
    结点的平衡因子是否计算正确。

【写法一代码:简单但是效率低】

// 求AVL树的高度
size_t _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;
}size_t Height()
{return _Height(_root);
}bool _IsBalance(Node* root)
{// 空树也是AVL树if (root == nullptr) return true;// 计算root节点的平衡因子:即root左右子树的高度差int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);// 如果计算出的平衡因子与pRoot的平衡因子不相等,或者// root平衡因子的绝对值超过1,则一定不是AVL树int diff = rightHeight - leftHeight;if (abs(diff) >= 2){cout << root->_kv.first << "不平衡" << endl;return false;}if (diff != root->_bf){cout << root->_kv.first << "平衡因子异常" << endl;return false;}// root的左和右如果都是AVL树,则该树一定是AVL树return _IsBalance(root->_left) && _IsBalance(root ->_right);
}

【写法二代码:效率高但是相较写法一难理解】

// 判断AVL树是否平衡,高效
bool _IsBalance(Node* root, int& height)
{// 空树也是AVL树if (root == nullptr){height = 0;return true;}// 后序递归,leftHeight、rightHeight会分别获取root的左右子树的高度int leftHeight = 0, rightHeight = 0;if (!_IsBalance(root->_left, leftHeight) || !_IsBalance(root->_right, rightHeight)){return false;}// 如果高度差的绝对值 >= 2,AVL树不平衡int diff = rightHeight - leftHeight;if (abs(diff) >= 2){cout << root->_kv.first << "不平衡" << endl;return false;}// 如果高度差 != root->_bf,AVL树插入过程中的平衡因子更新有问题if (diff != root->_bf){cout << root->_kv.first << "平衡因子异常" << endl;return false;}// 将root自己的高度通过引用返回给上一层栈帧height = leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;// root的左子树平衡、右子树平衡、root自身也平衡,那这棵AVL树就平衡return true;
}

参考文章

数据结构 —— 图解AVL树(平衡二叉树)
高度平衡二叉搜索树(AVLTree)
【数据结构】AVL树的删除(解析有点东西哦)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/748454.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

TSINGSEE青犀视频AI方案:数据+算力+算法,人工智能的三大基石

背景分析 随着信息技术的迅猛发展&#xff0c;人工智能&#xff08;AI&#xff09;已经逐渐渗透到我们生活的各个领域&#xff0c;从智能家居到自动驾驶&#xff0c;从医疗诊断到金融风控&#xff0c;AI的应用正在改变着我们的生活方式。而数据、算法和算力&#xff0c;正是构成…

【矩阵】48. 旋转图像【中等】

旋转图像 给定一个 n n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。你必须在 原地 旋转图像&#xff0c;这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,2,3],[4,5,6],[7,8,…

rust学习笔记(1-7)

原文 8万字带你入门Rust 1.包管理工具Cargo 新建项目 1&#xff09;打开 cmd 输入命令查看 cargo 版本 cargo --version2&#xff09; 使用 cargo new 项目名 在文件夹&#xff0c;按 shift 鼠标右键 &#xff0c;打开命令行&#xff0c;运行如下命令&#xff0c;即可创建…

基于springboot的七彩云南文化旅游网站的设计与实现(论文+源码)_kaic

摘 要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0c;在计算机上安装七彩云南文化旅游网站软件来发挥其高效地信息处理的作用&am…

【使用postman测试python接口】

打开python服务 设置postman如下&#xff0c;并发送&#xff1a; postman新建请求设置请求方式为post设置地址、raw、json方式、内容如下 结果&#xff1a; python如下&#xff1a; from flask import Flask, request, jsonifyapp Flask(__name__) # 实例化对象app.route…

你为什么是你,而不是别人?认识人格的力量

你为什么是你&#xff0c;而不是别人&#xff1f;让你做自我介绍&#xff0c;你会怎么描述自己呢&#xff1f; 人格心理学是心理学的一门重要分支学科。探求、描述和揭示个体思想、情绪及行为的独特模式&#xff0c;综合个人与环境诸多影响因素&#xff0c;对现实社会中的个人作…

Vite为什么比Webpack快

一、引言 主流的前端构建工具包括以下几种&#xff1a; Webpack&#xff1a;当下最热门的前端资源模块化管理和打包工具。它能够将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源。同时&#xff0c;Webpack还支持代码分割&#xff0c;可以按需加载模块&#…

Redis底层数据结构之String

文章目录 1. 前提回顾2. RedisObject三大数据类型简介3. SDS字符串4. SDS字符串源码分析5. 总结 1. 前提回顾 前面我们说到redis的String数据结构在底层有多种编码方式。例如我们执行下面两条语句 set k1 v1 set age 17我们查看类型&#xff0c;发现这类型都是String类型 我们…

docker desktop 启动失败

docker desktop 启动失败 1.如果直接启动报错&#xff0c;设置 2.如果启动报 Failed to set version to docker-desktop: exit code: -1的解决方法. netsh winsock reset

vue3+Ts项目按需引入Echarts,并封装成hooks

记录 vue3Ts 项目中&#xff0c;按需引入echarts并进行二次封装使用。 1、安装&#xff1a;npm i echarts 2、新增按需引入配置文件&#xff1a;echartsConfig.ts // 引入 echarts 核心模块&#xff0c;核心模块提供了 echarts 使用必须要的接口。 import * as echarts from …

3.Windows下安装MongoDB和Compass教程

Windows下安装MongoDB 总体体验下来&#xff0c;&#xff0c;要比MySQL的安装简单了许多&#xff0c;没有过多的配置&#xff0c;直接就上手了&#xff01; 1、下载 进入官方的下载页面https://www.mongodb.com/try/download/community&#xff0c;如下选择&#xff0c;我选…

第七节:使用SMB发布Web前端程序

一、概述 一直以来&#xff0c;多数人都使用Apache、IIS、Tomcat等开源或商业Web服务器来运行Web程序&#xff0c;各种参数太多&#xff0c;与我们简单易用逻辑相左。所以在架构设计的时候&#xff0c;我们也在考虑&#xff0c;我们公司的Web程序是否能运行在SMB中&#xff0c;…

52 硬中断的实现

前言 呵呵 中断机制 也是内核中很常见的机制了 中断机制是现代计算机系统中的基本机制之一&#xff0c;它在系统中起着通信网络的作用&#xff0c;以协调系统对各种外部事件的响应和处理&#xff0c;中断是实现多道程序设计的必要条件&#xff0c;中断是CPU 对系统发生的某个…

idea Springboot 在线考试管理系统开发mysql数据库web结构java编程计算机网页

一、源码特点 springboot 在线考试管理系统是一套完善的完整信息系统&#xff0c;结合mvc框架和bootstrap完成本系统springboot spring mybatis &#xff0c;对理解JSP java编程开发语言有帮助系统采用springboot框架&#xff08;MVC模式开发&#xff09;&#xff0c;系统具有…

关于-机器人学导论

机器人学导论&#xff0c;经典的基础教材&#xff0c;原书叫introduction to robotics mechanics and control &#xff0c;在网上看到这个下载&#xff0c;这个好像不像有些书已经免费了&#xff0c;但是太经典拿它当教材的很多&#xff0c;最好看清晰的纸质版&#xff0c;特别…

java-ssm-jsp基于java的信访管理系统的设计与实现

java-ssm-jsp基于java的信访管理系统的设计与实现 获取源码——》公主号&#xff1a;计算机专业毕设大全 获取源码——》公主号&#xff1a;计算机专业毕设大全

Gitlab CI/CD 自动化打包部署前端(vue)项目

一、虚拟机安装 1.vmware下载 2.镜像下载 3.Ubuntu 4.新建虚拟机 一直点下一步&#xff0c;直到点击完成。 5.分配镜像 二、Gitlab CI/CD 自动化部署项目 1.配置GitLab CI/CD&#xff1a; A.在你的Vue.js项目中&#xff0c;创建一个名为.gitlab-ci.yml的文件&#xff0…

windbg调试协议wireshark抓包解析插件

把目录下文件复制到如下位置,Wireshark支持版本4.0以上 C:\Program Files\Wireshark\plugins\4.0\kdnet.lua C:\Program Files\Wireshark\gcrypt.dll C:\Program Files\Wireshark\luagcrypt.dll 启动 “C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\windbg.exe” -k …

使用vscode——配置vue3用户代码片段

一、 设置/配置用户代码片段 二、点击新建全局代码片段文件输入vue.json 三、配置代码片段、 {"Print to console": {"prefix": "vue3","body": ["<template>"," <div></div>","</te…

express+mysql+vue,从零搭建一个商城管理系统14--快递查询(对接快递鸟)

提示&#xff1a;学习express&#xff0c;搭建管理系统 文章目录 前言一、安装md5&#xff0c;axios&#xff0c;qs二、新建config/logistics.js三、修改routes/order.js四、添加商品到购物车总结 前言 需求&#xff1a;主要学习express&#xff0c;所以先写service部分 快递鸟…