【C++】详解AVL树并模拟实现

  前言:

  •   之前我们为了让数据存储效率提高,引进了二叉搜索树。
  •   但是我们发现,二叉搜索树的时间复杂度还是O(N),因为二叉搜索树并不是非常的平衡。
  •   并不是所有树都是满二叉树,可能出现单边书这样极端的情况,所以我们引进了查找效率更高的AVL树

目录

(一)AVL树的概念

(二)AVL树的模拟实现

(1)AVL树结点的定义

(2)AVL树部分功能的实现

1、查找

2、插入(重点!)

2.1插入结点后平衡因子的变化

2.2情况分析

2.3旋转操作的实现

2.3.1左单旋

2.3.2右单旋

 2.3.3左右双旋

2.3.4右左双旋

(三) 验证AVL树

1、中序遍历

2、判断树是否平衡

(四)总代码详解

 



(一)AVL树的概念

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

  • 它的左右子树都是AVL树
  • 任何一颗子树左右子树高度之差(简称平衡因子)的绝对值不超过 1(-1 / 0 / 1)

图示:

图中结点外数字代表平衡因子→右子树高度-左子树高度

AVL树又叫高度平衡二叉搜索树。

如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 〇(logN)搜索时间复杂度 〇(logN)。


(二)AVL树的模拟实现

(1)AVL树结点的定义

~直接实现key_value的结构 – 三叉链的形式(带父节点)

图示如下: 

其中:

_bf-----balance factor,代表平衡因子

右子树与左子树的高度差


(2)AVL树部分功能的实现

1、查找

和二叉搜索树查找功能的实现几乎一致:

从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
最多查找高度次,走到到空,还没找到,这个值不存在。

2、插入(重点!)

  • 这里的插入思路和二叉搜索树中插入的思路一致,找到合适的位置之后再链接

这里链接是比较容易的,但是链接之后对各个结点中的平衡因子的调整则是比较费劲。

所以重点就在于分析各种可能出现平衡因子的情况,然后调整树来解决。

2.1插入结点后平衡因子的变化

1.首先我们考察插入结点和它父亲结点之间的关系变化:

  • 当一个结点的左或者右链接了一个结点,该结点为链接结点的父节点
  • 当该父节点的右边连接上孩子时,此时该父节点的右子树比左子树高了一层,平衡因子_bf + +
  • 当该父节点的左边连接上孩子时,此时该父节点的左子树比右子树高了一层,平衡因子_bf – –


2.然后我们再以此为基础考察其他结点的变化情况:

我们以上图为例,8结点作为父亲结点,平衡因子发生了变化,这就有可能继续导致8结点的父亲结点的平衡因子发生变化。

简而言之,插入一个结点真正会影响的是其祖先的平衡因子的改变。


3.处理方法

(1)向上更新:

  • 更新新插入节点祖先的平衡因子
  • 没有违反规则就结束了,违反规则,不平衡了就需要处理
  • 这里的处理是旋转处理(接下来会重点介绍)
  • 在更新的过程中只要是发现违反了AVL树规则的就需要旋转处理

(2)如何向上更新:

  • 更新的方式是沿着祖先路径更新(回溯)
  • 将parent结点更新到它的_parent位置上,将cur结点更新到它的_parent位置上
  • 在这个过程中一旦发现有违反AVL树规则的时即parent的平衡因子变成2或-2
  • 这时就需要进行旋转处理

具体过程如下:

  • 子树高度变了,就要继续往上更新
  • 子树的高度不变, 则更新完成
  • 子树违反平衡规则,则停止更新, 旋转子树

2.2情况分析

1.父亲结点平衡因子为0时,符合规则,break跳出:

2.父亲结点平衡因子是1或者-1时继续向上更新:

3.更新过程中,父亲结点平衡因子出现了2或者-2,说明子树不平衡了,需要上述说到的旋转处理(下文中会详解旋转的几种情况)

看了如上的代码有点抽象,我们下面用具体例子来讲解此情况下不同的情景:

我们也可以明晰的看出上述代码有四个条件语句,每个if其实代表的就是一种情景,我们详细分析:

情景一:

这时我们发现8作为父亲结点时出现了平衡因子为2的情况,此时cur结点平衡因子为1


 


情景二:

这时我们发现8作为父亲结点时出现了平衡因子为2的情况,此时cur结点平衡因子为-1


情景三,情景四大家可以自己画图啦!其实就是cur所在子树变为parent左子树的一些情况,下面详解旋转操作中将会对这几种情景解释并解决!

4.一定要检查 -- 不保证其他地方不会出现错误
            
    比如插入之前AVL数就存在平衡子树,|平衡因子| >= 2结点。


2.3旋转操作的实现

上述我们已经阐述了,在什么情况下需要对AVL树进行旋转操,接下来我们就来讲一下具体的旋转步骤。

旋转原则:

  • 保持搜索树的规则
  • 子树变平衡

旋转一共分为四种旋转方式:

  •  左单旋、右单旋
  •  左右双旋、右左双旋

2.3.1左单旋

当右子树高的时候,这时就要向左旋转。

旋转过程:

  • 将要旋转的子树的根节点设为parent,根结点的右子树为subR,subR的左节点为subRL
  • 将subRL给parent的右,再将parent给subR的左
  • 改变其链接关系即可
  • 这样一来subR做了子树的根,根结点的左右子树高度差从2变成了0

图示:

代码图解:(后续代码统一给)

2.3.2右单旋

 有左单旋的基础,我们知道右单旋的情况:
当左子树高的时候,这时就要向右旋转。

旋转过程:

  • 将要旋转的子树的根节点设为parent,根结点的左子树为subL,subL的右节点为subLR
  • 将subLR给parent的左,再将parent给subL的右
  • 改变其链接关系即可
  • 这样一来subL做了子树的根,根结点的左右子树高度差从2变成了0

图示:

代码图解:

 2.3.3左右双旋

在一些情况中,左单旋和右单旋是无法解决问题的,需要二次旋转来降低子树高度,

比如下面的情况:

这时候无论怎么单旋,我们都无法降低子树的高度。

所以这里我们要用到双旋。

图示:

这样一来,我们就把单次旋转无法降低高度的情况解决了!

详细过程可以跟着图示来理解。

代码图解: 

这里主要是根据subLR平衡因子的不同情况来给降低高度后子树中不同节点的平衡因子赋值,大家可以模仿上面的图示把不同情况画下来!

  •  我们在实现双旋的时候可以复用单旋
  •  但是单旋有个坑,会出现将平衡因子搞成0的情况

两种解决方案:

  • 将单旋中更新的平衡因子拿出来
  • 旋转之前将位置记录下来

我们采用第一种方法,单独将平衡因子拿出来处理。

2.3.4右左双旋

和上面的左右双旋的情况类似:

图示:

代码图解:
 


(三) 验证AVL树

上面我们基本完成了AVL树的两大重要的功能实现(删除操作有困难,本文暂不实现,后续深入会讨论),下面我们要验证我们写的树是不是AVL树。

主要验证两大方面:

  • 是不是二叉搜索树(AVL树是特殊的二叉搜索树,中序遍历应该是有顺序的)
  • 子树的高度差是不是符合条件(-2<右子树高度-左子树高度<2)

1、中序遍历

介绍前,我们先说明一点:

  • 我们平时调用类中的成员函数一般是对象.成员函数
  • 比如一个对象t中序历遍就是t.InOrder()
  • 这样符合库中其他类成员函数的使用习惯
  • 但是我们要想中序历遍必须传根节点
  • 所以我们叠加一层嵌套,巧妙解决这个问题:

多套一层,这样就可以符合我们使用这些接口函数的习惯了!

其实通过上面的讲解后,中序遍历作者已经给出啦:
我们选择和之前中序历遍搜索二叉树一样的方法,采用递归的方式解决

2、判断树是否平衡

我们一同思考,如何判断左右子树高度差在2以内呢?

  • 首先,我们必须知道左右子树的高度
  • 其次,我们再判断高度差是否是2以内

求树的高度:

其实我们之前也讲解过,采用递归的方法更容易来求解。

思路:树的高度就是左子树或者右子树高的那一颗树的高度加上根节点高度(也就是+1)

判断树是否平衡:

我们求解左右子树的高度后记录下来,相减的绝对值看是否是二以内。

当然,对于树中每一个结点我们都要判断他的子树是否符合条件,所以也要用到递归:

(四)总代码详解


#include<iostream>
#include<assert.h>
#include<algorithm>
using namespace std;
template<class K, class V>
struct AVLTreeNode
{AVLTreeNode<K, V>* _left;AVLTreeNode<K, V>* _right;AVLTreeNode<K, V>* _parent;pair<K, V> _kv;int _bf;//平衡因子AVLTreeNode(const pair<K, V>& kv):_left(nullptr),_right(nullptr),_parent(nullptr),_kv(kv),_bf(0){}
};template<class K, class V>
class AVLTree
{typedef AVLTreeNode<K, V> Node;
public:bool Insert(const pair<K, V>& kv){if (_root == nullptr){_root = new Node(kv);return true;}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;}else{return false;}}cur = new Node(kv);if (parent->_kv.first > kv.first){parent->_left = cur;}else{parent->_right = cur;}cur->_parent = parent;//更新平衡因子while (parent){if (cur == parent->_right){parent->_bf++;}else{parent->_bf--;}if (parent->_bf == 1 || parent->_bf == -1){parent = parent->_parent;cur = cur->_parent;}else if (parent->_bf == 0){break;}else if (parent->_bf == 2 || parent->_bf == -2){// 需要旋转处理 -- 1、让这颗子树平衡 2、降低这颗子树的高度//子树不平衡了 -- 需要旋转处理(左单旋的特征 -- 右边高)if (parent->_bf == 2 && cur->_bf == 1)//左单旋{RotateL(parent);}//子树不平衡了 -- 需要旋转处理(右单旋的特征 -- 左边高)else if (parent->_bf == -2 && cur->_bf == -1)//右单旋{RotateR(parent);}else if (parent->_bf == -2 && cur->_bf == 1)//左右双旋{RotateLR(parent);}else if (parent->_bf == 2 && cur->_bf == -1)//右左双旋{RotateRL(parent);}//旋转完之后ppNode为根的子树高度不变 -- 所以对ppNode的平衡因子没有影响break;}else // 一定要检查 -- 不保证其他地方不会出现错误{//插入之前AVL数就存在平衡子树,|平衡因子| >= 2结点assert(false);}}return true;}void InOrder(){_InOrder(_root);cout << endl;}bool IsBalance(){return _IsBalance(_root);}int Height(){return _Height(_root);}
private:bool _IsBalance(Node* root){if (root == nullptr)return true;int HeightL = _Height(root->_left);int HeightR = _Height(root->_right);if (HeightR - HeightL != root->_bf){cout << root->_kv.first << "平衡因子异常" << endl;return false;}return abs(HeightL - HeightR) < 2 && _IsBalance(root->_left) && _IsBalance(root->_right);空树也是AVL树//if (nullptr == root)//	return true;计算pRoot节点的平衡因子:即pRoot左右子树的高度差//int leftHeight = _Height(root->_left);//int rightHeight = _Height(root->_right);求差值//int diff = rightHeight - leftHeight;如果计算出的平衡因子与pRoot的平衡因子不相等,或者pRoot平衡因子的绝对值超过1,则一定不是AVL树//if (abs(diff) >= 2)//{//	cout << root->_kv.first << "结点平衡因子异常" << endl;//	return false;//}平衡因子没有异常但是和结点的对不上//if (diff != root->_bf)//{//	//说明更新有问题//	cout << root->_kv.first << "结点平衡因子不符合实际" << endl;//	return false;//}pRoot的左和右如果都是AVL树,则该树一定是AVL树把自己和自己的左右子树都检查了,递归检查//return _IsBalance(root->_left)//	&& _IsBalance(root->_right);}int _Height(Node* root){if (root == NULL)return 0;int leftH = _Height(root->_left);int rightH = _Height(root->_right);return leftH > rightH ? leftH + 1 : rightH + 1;}void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);cout << root->_kv.first << " ";_InOrder(root->_right);}//左单旋void RotateL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if (subRL){subRL->_parent = parent;}Node* ppnode = parent->_parent;subR->_left = parent;parent->_parent = subR;if (ppnode == nullptr){_root = subR;_root->_parent = nullptr;}else{if (ppnode->_left == parent){ppnode->_left = subR;}else{ppnode->_right = subR;}subR->_parent = ppnode;}parent->_bf = subR->_bf = 0;}//右单旋void RotateR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;if (subLR)subLR->_parent = parent;Node* ppnode = parent->_parent;subL->_right = parent;parent->_parent = subL;if (parent == _root){_root = subL;_root->_parent = nullptr;}else{if (ppnode->_left == parent){ppnode->_left = subL;}else{ppnode->_right = subL;}subL->_parent = ppnode;}parent->_bf = subL->_bf = 0;}void RotateLR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;RotateL(parent->_left);RotateR(parent);if (bf == 1){parent->_bf = 0;subLR->_bf = 0;subL->_bf = -1;}else if (bf == -1){parent->_bf = 1;subLR->_bf = 0;subL->_bf = 0;}else if (bf == 0){parent->_bf = 0;subLR->_bf = 0;subL->_bf = 0;}else{assert(false);}}void RotateRL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;RotateR(parent->_right);RotateL(parent);if (bf == 1){parent->_bf = -1;subRL->_bf = 0;subR->_bf = 0;}else if (bf == -1){parent->_bf = 0;subRL->_bf = 0;subR->_bf = 1;}else if (bf == 0){parent->_bf = 0;subRL->_bf = 0;subR->_bf = 0;}else{assert(false);}}private:Node* _root = nullptr;
};void Test_AVLTree1()
{//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };AVLTree<int, int> t1;for (auto e : a){/*	if (e == 14){int x = 0;}*/t1.Insert(make_pair(e, e));cout << e << "插入:" << t1.IsBalance() << endl;}t1.InOrder();cout << t1.IsBalance() << endl;
}void Test_AVLTree2()
{srand(time(0));const size_t N = 10;AVLTree<int, int> t;for (size_t i = 0; i < N; ++i){size_t x = rand() + i;t.Insert(make_pair(x, x));//cout << t.IsBalance() << endl;}t.InOrder();cout << t.IsBalance() << endl;cout << t.Height() << endl;
}

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

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

相关文章

应急三维电子沙盘数字孪生系统

一、简介应急三维电子沙盘数字孪生系统是一种基于虚拟现实技术和数字孪生技术的应急管理工具。它通过将真实世界的地理环境与虚拟世界的模拟环境相结合&#xff0c;实现了对应急场景的模拟、分析和决策支持。该系统主要由三维电子沙盘和数字孪生模型两部分组成。三维电子沙盘是…

【DRONECAN】(三)WSL2 及 ubuntu20.04 CAN 驱动安装

【DRONECAN】&#xff08;三&#xff09;WSL2 及 ubuntu20.04 CAN 驱动安装 前言 这一篇文章主要介绍一下 WSL2 及 ubuntu20.04 CAN 驱动的安装&#xff0c;首先说一下介绍本文的目的。 大家肯定都接触过 ubuntu 系统&#xff0c;但是我们常用的操作系统都是 Windows&#x…

计算机视觉的应用13-基于SSD模型的城市道路积水识别的应用项目

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下计算机视觉的应用13-基于SSD模型的城市道路积水识别的应用项目。今年第11号台风“海葵”后部云团的影响&#xff0c;福州地区的降雨量突破了历史极值&#xff0c;多出地方存在严重的积水。城市道路积水是造成交通拥…

本地电脑搭建web服务器、个人博客网站并发布公网访问 【无公网IP】(1)

文章目录 前言1. 安装套件软件2. 创建网页运行环境 指定网页输出的端口号3. 让WordPress在所需环境中安装并运行 生成网页4. “装修”个人网站5. 将位于本地电脑上的网页发布到公共互联网上 前言 在现代社会&#xff0c;网络已经成为我们生活离不开的必需品&#xff0c;而纷繁…

【MySQL】JDBC 编程详解

JDBC 编程详解 一. 概念二. JDBC 工作原理三. JDBC 使用1. 创建项目2. 引入依赖3. 编写代码(1). 创建数据源(2). 建立数据库连接(3). 创建 SQL(4). 执行 SQL(5). 遍历结果集(6). 释放连接 4. 完整的代码5. 如何不把 sql 写死 &#xff1f;6. 获取连接失败的情况 四. JDBC常用接…

Cmake入门(一文读懂)

目录 1、Cmake简介2、安装CMake3、单目录简单实例3.1、CMakeLists.txt3.2、构建bulid内部构建外部构建 3.3、运行C语言程序 4、多目录文件简单实例4.1、根目录CMakeLists.txt4.2、源文件目录4.3、utils.h4.4、创建build 5、链接外部库文件6、注意 1、Cmake简介 CMake是一个强大…

基于Hata模型的BPSK调制信号小区覆盖模拟matlab完整程序分享

基于Hata信道模型的BPSK调制信号小区覆盖模拟matlab仿真&#xff0c;对比VoIP, Live Video,FTP/Email 完整程序&#xff1a; clc; clear; close all; warning off; addpath(genpath(pwd)); % Random bits are generated here. bits randi([0, 1], [50,1]); M 2; t 1:1:50; …

sqlserver 查询数据显示行号

查询的数据需要增加一个行号 SELECT ROW_NUMBER() OVER(ORDER BY witd_wages_area ,witd_wages_type ,witd_department_id ,witd_give_out_time) 行号,ISNULL(witd_wages_area, 0) witd_wages_area ,witd_wages_type ,witd_department_id ,ISNULL(CONVERT(VARCHAR(7), witd_gi…

Json“牵手”当当网商品详情数据方法,当当商品详情API接口,当当API申请指南

当当网是知名的综合性网上购物商城&#xff0c;由国内著名出版机构科文公司、美国老虎基金、美国IDG集团、卢森堡剑桥集团、亚洲创业投资基金&#xff08;原名软银中国创业基金&#xff09;共同投资成立1。 当当网从1999年11月正式开通&#xff0c;已从早期的网上卖书拓展到网…

函数式接口:Java 中的函数式编程利器

文章目录 1. 函数式接口概念2. 注解3. 自定义函数式接口4. 函数式编程4.1 Lambda的延迟执行效果4.2 使用Lambda作为参数和返回值作为参数使用作为返回值使用 5. 常用的函数接口5.1 Supplier&#xff1a;生产者5.2 Consumer&#xff1a;消费者5.3 Predicate&#xff1a;判断5.4 …

薅羊毛零撸小游戏是这样赚米的!

薅羊毛小游戏作为一种特殊类型的游戏&#xff0c;吸引了一大批用户的关注。本文将探讨薅羊毛小游戏的盈利模式、用户体验以及对游戏产业的影响&#xff0c;旨在为读者提供专业而有深度的思考和启示。 一、薅羊毛小游戏的盈利模式&#xff1a; 1.广告变现&#xff1a;薅羊毛小游…

PageHelper分页原理解析

大家好&#xff0c;我是Leo! 今天给大家带来的是关于PageHelper原理的解析&#xff0c;最近遇到一个SQL优化的问题&#xff0c;顺便研究了一下PageHelper的原理&#xff0c;毕竟也是比较常用&#xff0c;源码也比较好看的懂&#xff0c;如果感兴趣的小伙伴可以跟着过程去DEBUG源…