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.hqwc.cn/news/542918.html

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

相关文章

配置vscode环境极简版(C/C++)(图文)

前言 众所周知&#xff0c;vscode是一个代码编辑器&#xff0c;不能直接编译运行我们敲的代码&#xff0c;必须提前配置好环境&#xff0c;而这也是劝退一众小白的一大重要因素&#xff0c;下面我想以一种提纲挈领的方式带大家走一遍从配置环境到运行实操代码的全过程。 安装…

布隆过滤器原理介绍和典型应用案例

整理自己过去使用布隆过滤器的应用案例和理解 基本介绍 1970年由布隆提出的一种空间效率很高的概率型数据结构&#xff0c;它可以用于检索一个元素是否在一个集合中&#xff0c;由只存0或1的位数组和多个hash算法, 进行判断数据 【一定不存在或者可能存在的算法】 如果这些…

【tls招新web部分题解】

emowebshell 非预期 题目提示webshell&#xff0c;就直接尝试一下常见的后门命名的规则 如 shell.php这里运气比较好&#xff0c;可以直接shell.php就出来 要是不想这样尝试的话&#xff0c;也可以直接dirsearch进行目录爆破 然后在phpinfo中直接搜素ctf或者flag就可以看到…

oracle基础-子查询 备份

一、什么是子查询 子查询是在SQL语句内的另外一条select语句&#xff0c;也被称为内查询活着内select语句。在select、insert、update、delete命令中允许是一个表达式的地方都可以包含子查询&#xff0c;子查询也可以包含在另一个子查询中。 【例1.1】在Scott模式下&#xff0…

F. Chat Screenshots

思路&#xff1a;拓扑排序&#xff0c;如果存在满足所有截图的顺序&#xff0c;那么这个图中就会存在拓扑排序&#xff0c;这意味着图中不会存在循环。因此&#xff0c;我们的目标就是检查图的非循环性。 代码&#xff1a; int b[200010], vis[200010], edge[200010]; vector&…

【Java,Redis】Redis 数据库存取字符串数据以及类数据

1、 字符串存取数据 Resource private StringRedisTemplate stringRedisTemplate;//从Redis中获取string字符串 stringRedisTemplate.opsForValue().get("cache:shop:"id); //Json -> class Shop shop JSONUtil.toBean(ShopJson,Shop.class); //字符串写入redis…

【漏洞复现】大华智慧园区综合管理平台SQL注入漏洞

Nx01 产品简介 大华智慧园区综合管理平台是一款综合管理平台&#xff0c;具备园区运营、资源调配和智能服务等功能。该平台旨在协助优化园区资源分配&#xff0c;满足多元化的管理需求&#xff0c;同时通过提供智能服务&#xff0c;增强使用体验。 Nx02 漏洞描述 大华智慧园区…

【Unity】程序创建Mesh(二)MeshRenderer、光照、Probes探针、UV信息、法线信息

文章目录 接上文MeshRenderer&#xff08;网格渲染器&#xff09;Materials&#xff08;材质&#xff09;Material和Mesh对应Lighting光照Lightmapping材质中的光照 光源类型阴影全局光照Probes&#xff08;探针&#xff09;Ray Tracing&#xff08;光线追踪&#xff09;Additi…

Dynamo3.0.0已来,未来可期~

Hello大家好&#xff01;我是九哥~ 有阵子没用Dynamo&#xff0c;最近偶然打开官网&#xff0c;发现最新版本都已经升级到了3.0.0。 看了下升级记录&#xff0c;是从2.19版本直接跳到了3.0.0&#xff0c;简单用了下&#xff0c;变化还是挺大的&#xff0c;未来可期啊~ …

非空约束

oracle从入门到总裁:​​​​​​https://blog.csdn.net/weixin_67859959/article/details/135209645 非空约束 所谓的非空约束&#xff0c;指的是表中的某一个字段的内容不允许为空。如果要使用非空约束&#xff0c;只需要在每个列的后面利用“NOT NULL”声明即可 -- 删除数…

使用Laravel开发项目

如何使用Laravel框架开发项目 一、安装Laravel框架 1.在安装Laravel框架钱我们需要先查看要安装的Laravel框架版本以及版本所需要的安装运行条件。 2.配置好安装环境后再安装Laravel框架 2.1.配置安装环境 1&#xff09;PHP版本 2&#xff09;PHP OpenSSL扩展 3&#xff…

docker容器技术基础入门-1

文章目录 容器(Container)传统虚拟化与容器的区别Linux容器技术Linux NamespacesCGroupsLXCdocker基本概念docker工作方式docker容器编排 容器(Container) 容器是一种基础工具&#xff1b;泛指任何可以用于容纳其他物品的工具&#xff0c;可以部分或完全封闭&#xff0c;被用于…