数据结构中的平衡搜索树 --- 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

相关文章

Python进行JSON和Excel文件转换处理指南

《Python进行JSON和Excel文件转换处理指南》在数据交换与系统集成中,JSON与Excel是两种极为常见的数据格式,本文将介绍如何使用Python实现将JSON转换为格式化的Excel文件,... 目录将 jsON 导入为格式化 Excel将 Excel 导出为结构化 JSON处理嵌套 JSON:

在macOS上安装jenv管理JDK版本的详细步骤

《在macOS上安装jenv管理JDK版本的详细步骤》jEnv是一个命令行工具,正如它的官网所宣称的那样,它是来让你忘记怎么配置JAVA_HOME环境变量的神队友,:本文主要介绍在macOS上安装... 目录前言安装 jenv添加 JDK 版本到 jenv切换 JDK 版本总结前言China编程在开发 Java

Spring Boot 中的默认异常处理机制及执行流程

《SpringBoot中的默认异常处理机制及执行流程》SpringBoot内置BasicErrorController,自动处理异常并生成HTML/JSON响应,支持自定义错误路径、配置及扩展,如... 目录Spring Boot 异常处理机制详解默认错误页面功能自动异常转换机制错误属性配置选项默认错误处理

SpringBoot 异常处理/自定义格式校验的问题实例详解

《SpringBoot异常处理/自定义格式校验的问题实例详解》文章探讨SpringBoot中自定义注解校验问题,区分参数级与类级约束触发的异常类型,建议通过@RestControllerAdvice... 目录1. 问题简要描述2. 异常触发1) 参数级别约束2) 类级别约束3. 异常处理1) 字段级别约束

一文解密Python进行监控进程的黑科技

《一文解密Python进行监控进程的黑科技》在计算机系统管理和应用性能优化中,监控进程的CPU、内存和IO使用率是非常重要的任务,下面我们就来讲讲如何Python写一个简单使用的监控进程的工具吧... 目录准备工作监控CPU使用率监控内存使用率监控IO使用率小工具代码整合在计算机系统管理和应用性能优化中,监

如何使用Lombok进行spring 注入

《如何使用Lombok进行spring注入》本文介绍如何用Lombok简化Spring注入,推荐优先使用setter注入,通过注解自动生成getter/setter及构造器,减少冗余代码,提升开发效... Lombok为了开发环境简化代码,好处不用多说。spring 注入方式为2种,构造器注入和setter

Java堆转储文件之1.6G大文件处理完整指南

《Java堆转储文件之1.6G大文件处理完整指南》堆转储文件是优化、分析内存消耗的重要工具,:本文主要介绍Java堆转储文件之1.6G大文件处理的相关资料,文中通过代码介绍的非常详细,需要的朋友可... 目录前言文件为什么这么大?如何处理这个文件?分析文件内容(推荐)删除文件(如果不需要)查看错误来源如何避

MySQL进行数据库审计的详细步骤和示例代码

《MySQL进行数据库审计的详细步骤和示例代码》数据库审计通过触发器、内置功能及第三方工具记录和监控数据库活动,确保安全、完整与合规,Java代码实现自动化日志记录,整合分析系统提升监控效率,本文给大... 目录一、数据库审计的基本概念二、使用触发器进行数据库审计1. 创建审计表2. 创建触发器三、Java

使用Python构建一个高效的日志处理系统

《使用Python构建一个高效的日志处理系统》这篇文章主要为大家详细讲解了如何使用Python开发一个专业的日志分析工具,能够自动化处理、分析和可视化各类日志文件,大幅提升运维效率,需要的可以了解下... 目录环境准备工具功能概述完整代码实现代码深度解析1. 类设计与初始化2. 日志解析核心逻辑3. 文件处

Java docx4j高效处理Word文档的实战指南

《Javadocx4j高效处理Word文档的实战指南》对于需要在Java应用程序中生成、修改或处理Word文档的开发者来说,docx4j是一个强大而专业的选择,下面我们就来看看docx4j的具体使用... 目录引言一、环境准备与基础配置1.1 Maven依赖配置1.2 初始化测试类二、增强版文档操作示例2.