数据结构进阶(C++) -- AVL树的实现

embedded/2024/11/25 17:58:26/

AVL树

概念

        1962年,前苏联的科学家G.M.Adelson-Velsky和E.M.Landis 在论文《An algorithm for the organization of information》中发表了AVL树。AVL树是最先发明的自平衡二叉搜索树,AVL树可以是一棵空树,也可以是具备下列性质的二叉搜索树:1、它的左右子树都是AVL树。2、左右子树的高度差的绝对值不超过1。由此可见,AVL树是一棵高度平衡的二叉搜索树,通过控制高度差来控制平衡。

        AVL树的实现中引入了一个平衡因子(balance factor)的概念,每个节点都有一个平衡因子,任何节点的平衡因子都等于该节点的右子树的高度减去左子树的高度,也就是说任何节点的平衡因子只有 0 / 1 / -1 三个取值,AVL树并不是必须要求使用平衡因子,但是有了平衡因子我们可以更方便的去观察和控制AVL树的平衡。

        这里要求平衡因子的绝对值不超过1,是因为有些情况下我们无法使高度差为0,比如一棵树只有 2 个节点时,根节点左右子树的高度差只能为 1 / -1 。AVL树整体节点数量和分布与完全二叉树类似,高度控制在logN,因此增删查改的效率也控制在了logN,相比于二叉搜索树有了本质上的提升。


 AVL树的实现

结构框架

#include<iostream>
#include<assert.h>
using namespace std;template<class K, class V>
class AVLTreeNode
{
public:pair<K, V> _kv;AVLTreeNode<K, V>* _parent;AVLTreeNode<K, V>* _left;AVLTreeNode<K, V>* _right;int _bf;AVLTreeNode(const pair<K,V>& kv):_kv(kv),_parent(nullptr),_left(nullptr),_right(nullptr),_bf(0){}
};template<class K, class V>
class AVLTree
{typedef AVLTreeNode<K, V> Node;
public:private:Node* _root = nullptr;
};
  • 我们首先定义了一个泛型的AVL树节点类(AVLTreeNode)和一个AVL树类(AVLTree)的基本框架,主要是用来构建一个支持自平衡功能的二叉树搜索树,依次结构与二叉搜索树相似。
  • AVLTreeNode 节点类的目的是作为AVL树的一个节点,存储键值对以及树结构的相关信息。其中成员变量有 _kv(存储键值对 pair<K, V>,用于二叉搜索树的查找和排序)、_parent(指向父节点的指针)、_left 和 _right(分别指向左右子节点的指针)、_bf(节点的平衡因子,表示左右子树的高度差)。构造函数(初始化成员变量)。
  •  AVLTree 树类目的是管理整个AVL树的结构,提供对节点值的插入、删除、平衡调整等操作。其中成员变量只有一个 _root(指向AVL树的根节点,缺省值为nullptr)。

AVL树的插入 

        这里的插入和二叉搜索树的思路一致,但因为引入了 _bf 平衡因子和 _parent 父节点的指针,所以会更加复杂。

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 (kv.first > cur->_kv.first){parent = cur;cur = cur->_right;}else if (kv.first < cur->_kv.first){parent = cur;cur = cur->_left;}else  // kv.first == cur->_kv.first{return false;}}//插入cur = new Node(kv);if (parent->_kv.first > kv.first){parent->_left = cur;}else{parent->_right = cur;}cur->_parent = parent;return true;
}

插入过程

  1. 插入一个值按二叉搜索树规则进行插入。
  2. 新增节点以后,只会影响祖先节点的高度,也就是可能会影响部分祖先节点的平衡因子,所以更新从新增节点到根节点这一路径上的所有平衡因子,实际中最坏情况就是更新到根节点,有些情况下更新到中间就会停止,下面会详细分析。
  3. 更新平衡因子过程没有出现问题就插入结束。
  4. 更新平衡因子过程出现不平衡,对不平衡子树旋转,旋转后再调平衡的同时,降低了子树的高度,不会再影响上一层,插入结束。

平衡因子

  • 平衡因子 = 右子树高度 - 左子树高度
  • 只有子树高度变化才会影响当前节点的平衡因子。
  • 插入节点会增加高度,新增节点在 parent 的右子树,则 parent 的平衡因子++,新增节点在 parent 的左子树,则 parent 的平衡因子--。
  • parent 所在子树的高度是否变化决定了是否会继续往上更新。

平衡因子更新结束条件

  • 更新后的 parent 的平衡因子等于 0 ,即更新中 parent 的平衡因子变化为 -1 -> 0 或 1 -> 0,说明更新前 parent 子树一边高一边低,新增的节点插入在低的那一边,插入后 parent 所在的子树高度不变,不会影响父亲节点的平衡因子,更新结束。
  • 更新后的 parent 的平衡因子等于 1 或 -1,即更新中 parent 的平衡因子变化为 0 -> 1 或 0 -> -1,说明更新前 parent 子树两边一样高,新增插入节点后,parent 所在的子树一辩稿一边低,parent 所在的子树符合平衡要求,但是高度增加了1,会影响 parent 的父节点的平衡因子,所以要继续向上更新。
  • 更新后 parent 的平衡因子等于 2 或 -2,即更新中 parent 的平衡因子变化为 1 -> 2 或 -1 -> -2 ,说明更新前 parent 子树一边高一边低,新增插入节点在高的那一边,parent 所在的子树高度差大于1,破坏了平衡,需要旋转处理,旋转的目的有两个:1、把 parent 子树旋转平衡。2、降低 parent 子树的高度,恢复到插入节点之前的高度。所以旋转后不需要继续向上更新,插入结束。
  • 如果全程没有遇到更新结束条件,更新到根节点后,根的平衡因子是 0 / 1 / -1 都会停止,是 2 / -2 会进行旋转。

如图所示:

 

 

代码演示:

	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走到空之后,插入新节点到parent的子节点cur = new Node(kv);if (parent->_kv.first < kv.first){parent->_right = cur;}else{parent->_left = cur;}//连接节点的父节点cur->_parent = parent;//更新平衡因子(balance factor)while (parent){if (cur == parent->_left)parent->_bf--;elseparent->_bf++;if (parent->_bf == 0){//左右子树高度差为0,更新结束break;}else if (parent->_bf == 1 || parent->_bf == -1){//继续向上更新cur = parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){//旋转处理break;}else{assert(false);}}return true;}

旋转

原则

        旋转的规则有两点:1、保持搜索树的规则。2、让旋转的AVL树从不平衡变平衡,其次是降低了旋转子树的高度。旋转总共分为四种:左单旋、右单旋、左右双旋和右左双旋。

右单旋

        使用右单旋的场景为插入节点后平衡因子为 subL->_bf == -1 和 parent->_bf == -2 的情况。我将右单旋的使用场景分为了两种,下图1展示的是10为根的树,插入结点参与旋转。下图2展示的是插入节点不参与旋转。

        核心旋转步骤:因为 5 < b子树的值 < 10,将b变为10的左子树,10变为5的右子树,5编程这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵树的高度恢复到了插入之前的高度,符合旋转原则。如果插入之前10是整棵树的一棵子树,那么旋转后不会影响上一层,只需要将旋转后的子树根节点连接到原父节点上即可,插入结束。

情况1:插入节点在后,插入结点参与旋转。

情况2:插入节点后,插入节点不参与旋转。

//右单旋
void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;//父节点和子节点的右子树形成子节点的新右子树parent->_left = subLR;if (subLR)subLR->_parent = parent;// 判断父节点是否为子树Node* pParent = parent->_parent;if (pParent == nullptr){_root = subL;}else{if (pParent->_left == parent)pParent->_left = subL;elsepParent->_right = subL;}subL->_parent = pParent;//子节点的右子树为原父节点subL->_right = parent;parent->_parent = subL;//更新平衡因子subL->_bf = parent->_bf = 0;
}

左单旋

        使用左单旋的场景为插入节点后平衡因子为 subR->_bf == 1 和 parent->_bf == 2 的情况。本质上与右单旋相同,只是旋转方向相反,下图为两情况的抽象版本。

        旋转核心步骤,因为10 < b子树的值 < 15,将b变成10的右子树,10变成15的左子树,15变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的高度恢复到了插入之前的高度,符合旋转原则。如果插入之前10整棵树的⼀个局部子树,旋转后不会再影响上⼀层,只需要将旋转后的子树根节点连接到原父节点上即可,插入结束。

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

左右双旋

        通过下图可以看到,左边高时,如果插入位置不是在a子树,而是插入在b子树,b子树高度从h变成h+1,引发旋转,右单旋无法解决问题,右单旋后,我们的树依旧不平衡。右单旋解决的纯粹的左边高,但是插入在b子树中,10为跟的子树不再是单纯的左边高,对于10是左边高,但是对于5是右边高,需要用两次旋转才能解决,以5为旋转点进行⼀个左单旋,以10为旋转点进行⼀个右单旋,这棵树这棵树就平衡了。

  •  上图分别为左右双旋中h==0和h==1具体场景分析,下面我们将a/b/子树抽象为高度h的AVL子树进行分析,另外我们需要把b子树的细节进一步展开为8和左子树高度为h-1的e和f子树,因为我们要对b的父亲5为旋转点进行左单旋,左单旋需要动b树中的左子树。b子树中新增结点的位置不同,平衡因子更新的细节也不同,通过观察8的平衡因子不同,这里我们要分三个场景讨论。
  • 场景1:h>=1时,新增结点插入在e子树,e子树高度从h-1并为h并不断更新8->5->10平衡因子,引发旋转,其中8的平衡因子为-1,旋转后8和5平衡因子为0,10平衡因子为1。
  • 场景2:h>=1时,新增结点插入在f子树,f子树高度从h-1变为h并不断更新8->5->10平衡因子,引发旋转,其中8的平衡因子为1,旋转后8和10平衡因子为0,5平衡因子为-1。
  • 场景3:h== 0时,a/b/c都是空树,b自己就是一个新增结点,不断更新5->10平衡因子,引发旋
    转,其中8的平衡因子为0,旋转后8和10和5平衡因子均为0。

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

右左双旋

  • 跟左右双旋类似,下面我们将a/b/c子树抽象为高度h的AVL子树进行分析,另外我们需要把b子树细节进一步展开为12和左子树高度为h-1的e和f子树,因为我们要对b的父亲15为旋转点进行右单旋,右单旋需要动b树中的右子树。b子树中新增结点的位置不同,平衡因子更新的细节也不同,过观察12的平衡因子不同,这里我们要分三个场景讨论。
  • 场景1: h >= 1时,新增结点插入在e子树,e 子树高度从h-1变为h并不断更新12->15->10平衡因子,引发旋转,其中12的平衡因子为-1,旋转后10和12平衡因子为0,15平衡因子为1。
  • 场景2: h >= 1时,新增结点插入在f子树,f 子树高度从h-1变为h并不断更新12->15->10平衡因子引发旋转,其中12的平衡因子为1,旋转后15和12平衡因子为0,10平衡因子为-1。
  • 场景3: h == 0时,a/b/c都是空树,b 自己就是一个新增结点,不断更新15->10平衡因子,引发旋转,其中12的平衡因子为0,旋转后10和12和15平衡因子均为0。

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

查找

Node* Find(const K& key)
{Node* cur = _root;while (cur){if (cur->_kv.first < key){cur = cur->_right;} else if (cur->_kv.first > key){cur = cur->_left;} else{return cur;}} return nullptr;
}

AVL树平衡检测

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

测试用例:

#include"AVLTree1.h"void TestAVLTree1()
{AVLTree<int, int> t;// 常规的测试⽤例//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };// 特殊的带有双旋场景的测试⽤例int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };for (auto& e : a){t.Insert({ e, e });} t.Inorder();//cout << t.IsBalanceTree() << endl;
}
int main()
{TestAVLTree1();return 0;
}

AVL树的删除

        AVL树的删除本章节不做讲解,有兴趣的同学可参考:《殷人昆 数据结构:用面向对象方法与C++语言描述》中讲解。


http://www.ppmy.cn/embedded/140449.html

相关文章

Centos环境安装Docker

一、Centos环境安装Docker 本文目录 一、Centos环境安装Docker1.1 在线安装Docker1、更新yum2、安装工具包3、设置镜像源4、安装前卸载原有的docker5、安装最新版本的docker ce6、启动docker7、设置开机启动docker8、安装好之后查看docker 版本9、配置容器镜像加速地址10、重载…

Nacos 与 Eureka 的区别

随着微服务架构的流行&#xff0c;服务发现成为了构建分布式系统的关键技术之一。在众多服务发现工具中&#xff0c;Nacos 和 Eureka 是两个非常受欢迎的选择。本文将深入探讨这两者的区别&#xff0c;帮助你在选择适合自己的服务发现解决方案时做出明智的决策。 如果你不懂得怎…

Go语言链接Redis数据库

1.使用go get命令安装go-redis/v8库&#xff1a; 我这里使用的vscode工具安装&#xff1a; go get github.com/go-redis/redis/v82.创建Redis客户端实例 使用以下Go代码连接到Redis服务器并执行命令&#xff1a; package mainimport ("context""fmt"&q…

基于物联网设计的人工淡水湖养殖系统(华为云IOT)_253

文章目录 一、前言1.1 项目介绍【1】项目开发背景【2】设计实现的功能【3】项目硬件模块组成【4】设计意义【5】国内外研究现状【6】摘要1.2 设计思路1.3 系统功能总结1.4 开发工具的选择【1】设备端开发【2】上位机开发1.5 参考文献1.6 系统框架图1.7 系统原理图1.8 实物图1.9…

学习记录:js算法(一百零二):使用最小花费爬楼梯

文章目录 使用最小花费爬楼梯思路一 使用最小花费爬楼梯 给你一个整数数组 cost &#xff0c;其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用&#xff0c;即可选择向上爬一个或者两个台阶。 你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。 请你…

C51数字时钟/日历---LCD1602液晶显示屏

题目要求&#xff1a; 数字电子日历/时钟设计 设计一个基于MCS51的电子日历和时钟。 基本要求 &#xff08;1&#xff09; 可通过按键在日历和时间之间切换显示&#xff1b; &#xff08;2&#xff09; 可由按键调整日期和时间 &#xff08;3&#xff09; 可整点报时&#x…

Scala全文单词统计

一&#xff1a;方法 package test5 import java.io.PrintWriter import scala.io.Source //可变的Map import scala.collection.mutable object test5_1 {def main(args: Array[String]): Unit { //1.读入文件val content Source.fromFile("1.txt").mkString // …

python中的base64使用小笑话

在使用base64的时候将本地的图片转换为base64 代码如下&#xff0c;代码绝对正确 import base64 def image_to_data_uri(image_path):with open(image_path, rb) as image_file:image_data base64.b64encode(image_file.read()).decode(utf-8)file_extension image_path.sp…