数据结构中的平衡搜索树 --- AVL树是怎样进行旋转处理的?(平衡因子版本)

本文主要是介绍数据结构中的平衡搜索树 --- AVL树是怎样进行旋转处理的?(平衡因子版本),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

前言

        搜索二叉树

AVL树

节点的定义

插入

旋转


前言

搜索二叉树

搜索二叉树 又称 二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  • 它的左右子树也分别为二叉搜索树

示例:

具体可以查看搜索二叉树

 但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树(或接近单只树),二叉搜索树的性能就失去了,并且 时间复杂度会退化成O(N),因此需要对底层结构进行平衡处理,即采用平衡树(AVL树、红黑树)来实现,使二叉搜索树的性能都能达到最优.

AVL树

AVL树的概念

        二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查 找元素相当于在顺序表中搜索元素,效率低下 。因此,两位俄罗斯的数学家 G.M.Adelson-Velskii和E.M.Landis 1962 年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右 子树高度之差的绝对值不超过 1( 需要对树中的结点进行调整 ) ,即可降低树的高度,从而减少平均搜索长度。

一棵 AVL 树或者是空树,或者是具有以下性质的二叉搜索树:
  • 它的左右子树都是AVL
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1) --- 一般是右子树减去左子树等于根

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

AVL树节点的定义
template<class K, class V>
class AVLTreeNode
{AVLTreeNode<K,V>* _left;	//左子树节点AVLTreeNode<K, V>* _right;	//右子树节点AVLTreeNode<K, V>* _parent;	//父节点pair<K, V> _kv;int _bf; //balance factor :平衡因子AVLTreeNode(const pair<K, V>& kv):_left(nullptr), _right(nullptr), _parent(nullptr),_kv(kv),_bf(0){}
};
AVL树的插入
AVL 树就是在二叉搜索树的基础上引入了平衡因子,因此 AVL 树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:
  • 按照二叉搜索树的方式插入新节点
  •  调整节点的平衡因子
template<class K,class V>
class AVLTree
{typedef AVLTreeNode<K, V> Node;
public:bool insert(const pair<K, V>& kv){//_root为空if (_root == nullptr) {_root = new Node(kv);return true;}//_root不为空Node* cur = _root;Node* parent = nullptr;   //记录cur的父节点,方便进行链接while (cur){if (kv.first < cur->_kv.first)  //插入的值小于存储的值{parent = cur;cur = cur->_left;}else if(kv.first > cur->_kv.first) //插入的值大于存储的值{parent = cur;cur = cur->_right;}else{return false; //相等,则插入失败}}//当前位置为空,插入的值与原本的值不相等,进行链接cur = new Node(kv);if (kv.first < parent->_kv.first){parent->_left = cur;}else{parent->_right = cur;}cur->_parent = parent;  //需要注意,进行链接//链接之后,此时需要更新平衡因子//.......return true;}private:Node* _root = nullptr;};

此时怎样调整节点的平衡因子呢?

观察一下平衡因子的性质: 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)

                                                                        而且平时我们习惯使用右子树高度减去左子树高度等于根

可以得出:如果新增节点是右子树,那么父节点需要++;如果新增节点是左子树,那么父节点需要 --

cur = parent->_right;     parent->_bf++;

cur = parent->_left;       parent->_bf--;

示例1:

示例2:

那么此时产生的新问题是,当父节点更新后,要不要继续向上更新?或者什么决定了要不要继续向上更新???

观察示例1与示例2可以得出,如果parent节点的高度发生了变化,那么是需要继续更新的,如果parent的高度没有发生变化,那么就不需要继续更新。

  • 情况1:parent->_bf == 1 || parent->_bf == -1      ---    需要继续向上更新,因为说明插入之前 parent->_bf == 0  ,表示插入之前父节点两边的高度相等,现在有一边插入了一个新节点,此时高度发生了改变,所以需要继续向上更新

  • 情况2:parent->_bf == 0     ---    不用向上更新,因为说明插入之前 parent->_bf == 1 || parent->_bf == -1,表示插入之前父节点两边的高度不相等,现在矮的一边插入了一个新节点,此时高度平衡,所以不用向上更新。

  • 情况3:parent->_bf == 2 || parent->_bf == -2   ---   所在子树高度不平衡,需要进行旋转处理
template<class K,class V>
class AVLTree
{typedef AVLTreeNode<K, V> Node;
public:bool insert(const pair<K, V>& kv){//_root为空if (_root == nullptr) {_root = new Node(kv);return true;}//_root不为空Node* cur = _root;Node* parent = nullptr;   //记录cur的父节点,方便进行链接while (cur){if (kv.first < cur->_kv.first)  //插入的值小于存储的值{parent = cur;cur = cur->_left;}else if(kv.first > cur->_kv.first) //插入的值大于存储的值{parent = cur;cur = cur->_right;}else{return false; //相等,则插入失败}}//当前位置为空,插入的值与原本的值不相等,进行链接cur = new Node(kv);if (kv.first < parent->_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 == 0){break; }else if (parent->_bf == 1 || parent->_bf == -1){//继续向上更新parent = parent->_parent;cur = cur->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){//需要进行旋转处理//........}else{//程序走到这里说明问题很严重,直接断言assert(false);}}return true;}private:Node* _root = nullptr;
};

那么什么情况下会出现旋转处理???

1.更新平衡因子:如果更新完成,平衡因子没有出现问题 | _bf l <= 1,平衡结构没有受到影响,不需要处理
 

2.更新平衡因子:如果更新完成,平衡因子出现问题 | _bf | > 1,平衡结构受到影响,需要处理(旋转)

AVL树的旋转

   如果在一棵原本是平衡的AVL 树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。所以旋转的本质有两点:
  • 1.让这棵子树平衡
  • 2.降低这棵子树的高度
根据节点插入位置的不同,AVL 树的旋转分为四种:

1.新节点插入较高右子树的右侧--- 右右:左单旋
抽象图:

旋转的过程:
  • ① b变成了30的右边
  • ② 30变成60的左边
  • ③ 60变成整棵树的根
     
在旋转过程中,有以下几种情况需要考虑:
  • ① 60节点的左孩子可能存在,也可能不存在
  • ② 30可能是根节点,也可能是子树

如果是根节点,旋转完成后,要更新根节点

 如果是子树,可能是某个节点的左子树,也可能是右子树

具象图:

当h == 0:

当h == 1:

当h == 2:
 

示例:

变量定义:

代码:
	//左单旋void RotateL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if (subRL)subRL->_parent = parent;//提前记录祖先节点Node* pparent = parent->_parent;subR->_left = parent;parent->_parent = subR;//值得注意的是,parent节点不一定为根节点,也就是旋转的可能是一棵子树而不是整棵树if (pparent == nullptr) //意味着parent节点是根节点{_root = subR;_root->_parent = nullptr;}else{//判断parent 在 祖先节点的左还是右if (pparent->_right == parent){pparent->_right = subR;}else{pparent->_left = subR;}subR->_parent = pparent;  //更改subR的父节点}//注意:一定要更新平衡因子parent->_bf = subR->_bf = 0;}

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;subL->_right = parent;parent->_parent = subL;if (parent == _root) {_root = subL;_root->_parent = nullptr;}else{//判断parent 在 祖先节点的左还是右if (pparent->_right == parent){pparent->_right = subL;}else{pparent->_left = subL;}subL->_parent = pparent;  //更改subR的父节点}//注意:一定要更新平衡因子parent->_bf = subL->_bf = 0;}

3. 新节点插入较高左子树的右侧 --- 左右:先左单旋再右单旋
抽象图:
代码演示:
	void RotateLR(Node* parent){RotateL(parent->_left);  //左旋RotateR(parent);		 //右旋}

这样可以嘛?其实有个非常严重的错误,就是无论左旋还是右旋函数最后都会把parent ,subR,subL的平衡因子置成0

        以上面的图为例(新增节点在subLR的左子树):第一次单旋会把30节点 、60节点 的平衡因子置成 0,第二次单旋会把60节点 、90节点 的平衡因子置成 0 ,这显然是不对的,因为90节点最后的平衡因子应该是1。所以需要分情况讨论:

新增节点在subLR的右子树

一般来说最后一种不需要考虑,因为都会被单旋修改为0,但是建议不要依赖单旋

总结这里不能依靠左旋or右旋函数来修改平衡因子,需要手动修改

代码如下:

//左右双旋void RotateLR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;//因为双旋过后 _bf 都会被修改为0,所以需要提前记录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 = 1;subL->_bf = 0;subLR->_bf = 0;}else if (bf == 1){parent->_bf = 0;subL->_bf = -1;subLR->_bf = 0;}else{assert(false);  //依旧直接断言,走到这里说明程序出现严重错误}}

4. 新节点插入较高右子树的左侧 --- 右左:先右单旋再左单旋

(参考左右双旋)

抽象图:

代码:

//右左双旋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 = 0;subR->_bf = 1;subRL->_bf = 0;}else if (bf == 1){parent->_bf = -1;subR->_bf = 0;subRL->_bf = 0;}else{assert(false);  }}

那么什么时候左旋?什么时候右旋,什么时候双旋呢???

观察上面4种旋转的情况可以知道:

  • 左子树高 - 右旋 
  • 右子树高 - 左旋   
  • 左子树高,左子树的右孩子高 - 左右双旋
  • 右子树高,右子树的左孩子高 - 右左双旋

 AVL 树的删除
        因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不错与删除不同的时,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。
AVL 树的性能
        AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过 1 ,这样可以保证查询时高效的时间复杂度,即$log_2 (N)$ 。但是如果要对 AVL 树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的( 即不会改变 ) ,可以考虑 AVL 树,但一个结构经常修改,就不太适合。

最后附上完整代码以及测试一棵树是否是AVL树的方法:

                                                                                                                AVLTree.h

#pragma once
#include <iostream>
#include <assert.h>
#include <string>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; //balance factor :平衡因子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:void InOrder(){_Inorder(_root);cout << endl;}bool Insert(const pair<K, V>& kv){//_root为空if (_root == nullptr) {_root = new Node(kv);return true;}//_root不为空Node* cur = _root;Node* parent = nullptr;   //记录cur的父节点,方便进行链接while (cur){if (kv.first < cur->_kv.first)  //插入的值小于存储的值{parent = cur;cur = cur->_left;}else if (kv.first > cur->_kv.first) //插入的值大于存储的值{parent = cur;cur = cur->_right;}else{return false; //相等,则插入失败}}//当前位置为空,插入的值与原本的值不相等,进行链接cur = new Node(kv);if (kv.first < parent->_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 == 0){break; }else if (parent->_bf == 1 || parent->_bf == -1){//继续向上更新parent = parent->_parent;cur = cur->_parent;}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);}else{assert(false);}break;  //旋转之后是可以直接跳出循环的,旋转之后(不管是整棵树还是子树)都是平衡的}else{//程序走到这里说明问题很严重,直接断言assert(false);}}return true;}//判断是否为AVL树bool IsBalance(){return _IsBalance(_root);}protected: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* pparent = parent->_parent;subR->_left = parent;parent->_parent = subR;//值得注意的是,parent节点不一定为根节点,也就是旋转的可能是一棵子树而不是整棵树if (pparent == nullptr) //意味着parent节点是根节点{_root = subR;_root->_parent = nullptr;}else{//判断parent 在 祖先节点的左还是右if (pparent->_right == parent){pparent->_right = subR;}else{pparent->_left = subR;}subR->_parent = pparent;  //更改subR的父节点}//注意:一定要更新平衡因子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* pparent = parent->_parent;subL->_right = parent;parent->_parent = subL;if (parent == _root) {_root = subL;_root->_parent = nullptr;}else{//判断parent 在 祖先节点的左还是右if (pparent->_right == parent){pparent->_right = subL;}else{pparent->_left = subL;}subL->_parent = pparent;  //更改subR的父节点}//注意:一定要更新平衡因子parent->_bf = subL->_bf = 0;}//左右双旋void RotateLR(Node* parent){//记录修改平衡因子的位置Node* subL = parent->_left;Node* subLR = subL->_right;//因为双旋过后bf都会被修改为0,所以需要提前记录subLR的平衡因子int bf = subLR->_bf;RotateL(parent->_left);RotateR(parent);//分三种情况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;}else if (bf == 0){parent->_bf = 0;subL->_bf = 0;subLR->_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 == 0){parent->_bf = 0;subR->_bf = 0;subRL->_bf = 0;}else if (bf == -1){parent->_bf = 0;subR->_bf = 1;subRL->_bf = 0;}else if (bf == 1){parent->_bf = -1;subR->_bf = 0;subRL->_bf = 0;}else{assert(false);}}//计算高度int _Height(Node* root){if (root == nullptr)return 0;int leftH = _Height(root->_left);  //左子树高度int rightH = _Height(root->_right); //右子树高度return leftH > rightH ? leftH + 1 : rightH + 1;  //返回的是整棵树的高度}//判断是否是AVL树 - 子函数bool _IsBalance(Node* root){if (root == nullptr)return true;int left_h = _Height(root->_left);int right_h = _Height(root->_right);//检查平衡因子if (right_h - left_h != root->_bf){cout << root->_kv.first << "节点平衡因子异常" << endl;return false;}//判断左右子树之间的差是否 < 2  abs:求绝对值return abs(left_h - right_h) < 2&& _IsBalance(root->_left)&& _IsBalance(root->_right);}protected:Node* _root = nullptr;
};void Test_AVLTree1()
{int arr1[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };int arr2[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };AVLTree<int, int> t1;for (auto e : arr1){t1.Insert(make_pair(e, e));cout << e << "插入:" << t1.IsBalance() << endl;  //插入进行检查}t1.InOrder();cout << t1.IsBalance() << endl;
}

这篇关于数据结构中的平衡搜索树 --- AVL树是怎样进行旋转处理的?(平衡因子版本)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/802475

相关文章

如何使用celery进行异步处理和定时任务(django)

《如何使用celery进行异步处理和定时任务(django)》文章介绍了Celery的基本概念、安装方法、如何使用Celery进行异步任务处理以及如何设置定时任务,通过Celery,可以在Web应用中... 目录一、celery的作用二、安装celery三、使用celery 异步执行任务四、使用celery

SpringBoot操作spark处理hdfs文件的操作方法

《SpringBoot操作spark处理hdfs文件的操作方法》本文介绍了如何使用SpringBoot操作Spark处理HDFS文件,包括导入依赖、配置Spark信息、编写Controller和Ser... 目录SpringBoot操作spark处理hdfs文件1、导入依赖2、配置spark信息3、cont

SpringBoot使用minio进行文件管理的流程步骤

《SpringBoot使用minio进行文件管理的流程步骤》MinIO是一个高性能的对象存储系统,兼容AmazonS3API,该软件设计用于处理非结构化数据,如图片、视频、日志文件以及备份数据等,本文... 目录一、拉取minio镜像二、创建配置文件和上传文件的目录三、启动容器四、浏览器登录 minio五、

你的华为手机升级了吗? 鸿蒙NEXT多连推5.0.123版本变化颇多

《你的华为手机升级了吗?鸿蒙NEXT多连推5.0.123版本变化颇多》现在的手机系统更新可不仅仅是修修补补那么简单了,华为手机的鸿蒙系统最近可是动作频频,给用户们带来了不少惊喜... 为了让用户的使用体验变得很好,华为手机不仅发布了一系列给力的新机,还在操作系统方面进行了疯狂的发力。尤其是近期,不仅鸿蒙O

什么是 Ubuntu LTS?Ubuntu LTS和普通版本区别对比

《什么是UbuntuLTS?UbuntuLTS和普通版本区别对比》UbuntuLTS是Ubuntu操作系统的一个特殊版本,旨在提供更长时间的支持和稳定性,与常规的Ubuntu版本相比,LTS版... 如果你正打算安装 Ubuntu 系统,可能会被「LTS 版本」和「普通版本」给搞得一头雾水吧?尤其是对于刚入

windows端python版本管理工具pyenv-win安装使用

《windows端python版本管理工具pyenv-win安装使用》:本文主要介绍如何通过git方式下载和配置pyenv-win,包括下载、克隆仓库、配置环境变量等步骤,同时还详细介绍了如何使用... 目录pyenv-win 下载配置环境变量使用 pyenv-win 管理 python 版本一、安装 和

python-nmap实现python利用nmap进行扫描分析

《python-nmap实现python利用nmap进行扫描分析》Nmap是一个非常用的网络/端口扫描工具,如果想将nmap集成进你的工具里,可以使用python-nmap这个python库,它提供了... 目录前言python-nmap的基本使用PortScanner扫描PortScannerAsync异

MyBatis延迟加载的处理方案

《MyBatis延迟加载的处理方案》MyBatis支持延迟加载(LazyLoading),允许在需要数据时才从数据库加载,而不是在查询结果第一次返回时就立即加载所有数据,延迟加载的核心思想是,将关联对... 目录MyBATis如何处理延迟加载?延迟加载的原理1. 开启延迟加载2. 延迟加载的配置2.1 使用

Android WebView的加载超时处理方案

《AndroidWebView的加载超时处理方案》在Android开发中,WebView是一个常用的组件,用于在应用中嵌入网页,然而,当网络状况不佳或页面加载过慢时,用户可能会遇到加载超时的问题,本... 目录引言一、WebView加载超时的原因二、加载超时处理方案1. 使用Handler和Timer进行超

Python中处理NaN值的技巧分享

《Python中处理NaN值的技巧分享》在数据科学和数据分析领域,NaN(NotaNumber)是一个常见的概念,它表示一个缺失或未定义的数值,在Python中,尤其是在使用pandas库处理数据时,... 目录NaN 值的来源和影响使用 pandas 的 isna()和 isnull()函数直接比较 Na