• 数据结构之< AVLTree >


    🌈前言

    本篇文章进行数据结构中AVL树的学习!!!

    🌷1、AVLTree的概念

    AVL树的由来:

    • 前面讲了二叉搜索树的不足之处,它虽然可以缩短搜索效率,但是最坏情况会退化成单支树。所以就有了二叉平衡搜索树(AVLTree),它解决了这个弊端,让树保持平衡

    历史:

    • 俄罗斯的数学家 G.M.Adelson-Velskii 和 E.M.Landis 在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度

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

    1. 它的左右子树都是AVL树
    2. 左右子树高度之差(简称“平衡因子”)的绝对值不超过2/-2(可以为0/1/-1)
      在这里插入图片描述

    注意:

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

    🌹1.2、AVLTree节点的定义

    template <typename K, typename V>
    struct AVLTNode
    {
    	AVLTNode(const pair<K, V>& _kv)
    		: kv(_kv)
    		, left(nullptr)
    		, right(nullptr)
    		, parent(nullptr)
    		, _bf(0)
    	{}
    
    	pair<K, V> kv; // 存储节点值 -- first and second
    	AVLTNode<K, V>* left;
    	AVLTNode<K, V>* right;
    	AVLTNode<K, V>* parent;
    	int _bf; // 平衡因子,控制树的平衡
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    注意:

    1. 这里的AVLTree节点存储的值是"键值对"
    2. AVLTree是通过三叉链来控制的,二叉链也可以,就是麻烦了一些
    3. _bf是每个节点的平衡因子,用来控制树的平衡

    🌿1.2、AVLTree的插入

    AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:

    1. 按照二叉搜索树的方式插入新节点(升序:值比根小插入左边,反之右边)
    2. 调整节点的平衡因子

    • 按搜索树规则插入,插入后链接双亲节点(parent)
    	bool insert(const pair<K, V>& kv)
    	{
    		if (root == nullptr) {
    			root = new Node(kv);
    			root->_bf = 0;
    			return true;
    		}
    		Node* cur = root;
    		Node* parent = nullptr;
    
    		// 按二叉搜索树的特点进行插入
    		while (cur)
    		{
    			if (cur->kv.first > kv.first) {
    				parent = cur;
    				cur = cur->left;
    			}
    			else if (cur->kv.first < kv.first) {
    				parent = cur;
    				cur = cur->right;
    			}
    			else {
    				return false;
    			}
    		}
    		Node* newNode = new Node(kv);
    		cur = newNode; // 更新cur,待链接父节点
    		if (parent->kv.first > kv.first) {
    			parent->left = newNode;
    		}
    		else {
    			parent->right = newNode;
    		}
    		cur->parent = parent;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    注意:按二叉搜索树规则插入,AVLTree为三叉链,链接新节点需要更新双亲节点

    在这里插入图片描述


    • 插入后调整平衡因子,平衡因子可以是0/1/-1,如果为2/-2需要旋转调整
    while (parent) // 一直向上回溯调整到祖先结束
    {
    	// 如果插入节点在左边,则双亲节点平衡因子减1,右边加1
    	if (cur == parent->left) {
    		--parent->_bf;
    	}
    	else {
    		++parent->_bf;
    	}
    
    	// 如果双亲节点平衡因子为0,则不需要调整 -- 1 or -1 > 0 -- 插入节点填补了矮的节点(插入左边,且右边有节点)
    	if (parent->_bf == 0) {
    		break;
    	}
    
    	// 如果父节点平衡因子为1/-1,则继续调整 -- 0 > 1 or -1插入节点导致双亲节点变更或变矮(插入左边,且右边无节点)
    	else if (parent->_bf == 1 || parent->_bf == -1) 
    	{
    		cur = cur->parent;
    		parent = parent->parent;
    	}
    	// 后面讲旋转会处理
    	else if (parent->_bf == 2 || parent->_bf == -2) 
    	{}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    注意:

    • 如果cur插入到parent的左侧,只需给parent的平衡因子-1即可
    • 如果cur插入到parent的右侧,只需给parent的平衡因子+1即可
    • 平衡因子为1/-1时,需要更新cur和parent继续向上调整平衡因子
    • 平衡因子为0时,说明这棵树已平衡,跳出循环
      在这里插入图片描述

    🌺1.3、AVLTree的旋转

    如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:

    1. 新节点插入较高右子树的右侧 — 右右:左单旋(根平衡因子为2,右子树为1)
      在这里插入图片描述

    重点:展开抽象图的几种情况

    在这里插入图片描述

    	// 左单旋 -- 新节点插入较高右子树的右侧 -- 右右
    	void RotateL(Node* parent)
    	{
    		Node* subR = parent->right;
    		Node* subRL = subR->left;
    
    		parent->right = subRL;
    		if (subRL != nullptr) {
    			subRL->parent = parent; // 如果左节点为空,则会造成空指针访问
    		}
    		Node* PpNode = parent->parent; // 后面parent不为根节点时,需要保存parent01的双亲节点进行链接
    
    		subR->left = parent;
    		parent->parent = subR;
    
    		// 一开始根节点可能是parent,旋转后要进行处理 -- 根应改为subR,根的双亲节点为nullptr
    		if (parent == root) {
    			root = subR;
    			root->parent = nullptr;
    		}
    		else {
    			// parent可能为整颗树的局部子树 -- 旋转后树的结构就乱了,需要将parent的双亲节点指向正确的位置
    			if (parent == PpNode->left) {
    				PpNode->left = subR;
    			}
    			else {
    				PpNode->right = subR;
    			}
    			subR->parent = PpNode;
    		}
    
    		// 更新平衡因子
    		parent->_bf = 0;
    		subR->_bf = 0;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    注意:

    • 旋转时,需要注意双亲节点要重新调整,很多人会忘记
    • 旋转后,subRL的双亲节点需要链接parent,subRL可能为空,需要判空
    • 如果根为旋转节点,旋转后subR为根,需要更新root和它的双亲
    • 如果为局部子树,需要保存parent的父节点来链接subR,因为旋转后无法找到它
      在这里插入图片描述

    1. 新节点插入较高左子树的左侧—左左:右单旋(根节点平衡因子为-2,左子树为-1)在这里插入图片描述
      重点:展开抽象图的几种情况

    在这里插入图片描述

    	// 右单旋 -- 新节点插入较高左子树的左侧 -- 左左
    	void RotateR(Node* parent)
    	{
    		// 旋转方法与左单旋一致
    		Node* subL = parent->left;
    		Node* subLR = subL->right;
    
    		parent->left = subLR;
    		if(subLR != nullptr)
    			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 = 0;
    		subL->_bf = 0;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    注意:右旋与左旋差不多这里就不画图了,参考左旋

    • parent的左节点链接subLR
    • subL的右子树链接parent,其余细节问题根左旋一样

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

    在这里插入图片描述
    重点:展开抽象图的几种情况

    在这里插入图片描述

    	// 左右双旋 -- 左子树高,左子树的右子树矮 subL: 1 subLR: -1
    	void RotateLR(Node* parent)
    	{
    		// 记录平衡因子和节点,待更新
    		Node* subL = parent->left;
    		Node* subLR = subL->right;
    		int bf = subLR->_bf;
    
    		// 先subL为根进行旋转,然后通过parent为根进行右旋
    		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;
    		}
    		else {
    			assert(false);
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    注意:

    • 将双旋变成单旋后再旋转,即:先对30进行左单旋,然后再对90进行右单旋,旋转完成后再考虑平衡因子的更新
    • 双旋的难点不是旋转,而是更新平衡因子,不画图容易出错

    在这里插入图片描述


    1. 新节点插入较高右子树的左侧 — 右左:先右单旋再左单旋
    • 下面直接列举旋转后更新平衡因子的三种情况:(h == 0)的就不画了
      在这里插入图片描述
    	// 右左双旋 -- subR: 1 subRL: -1
    	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;
    		}
    		else {
    			assert(false);
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 总结:

    假如以parent为根的子树不平衡,即parent的平衡因子为2或者-2,分以下情况考虑:

    1. parent的平衡因子为2,说明parent的右子树高,设parent的右子树的根为subR
    • 当subR的平衡因子为1时,执行左单旋
    • 当subR的平衡因子为-1时,执行右左双旋
    1. parent的平衡因子为-2,说明pParent的左子树高,设parent的左子树的根为subL
    • 当subL的平衡因子为-1是,执行右单旋
    • 当subL的平衡因子为1时,执行左右双旋

    旋转完成后,原parent为根的子树个高度降低,已经平衡,不需要再向上更新


    🌻1.4、AVLTree的删除(了解)

    • 因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不
      错与删除不同的时,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。

    • 具体实现可参考《算法导论》或《数据结构-用面向对象方法与C++描述》殷人昆版


    🌻1.5、AVLTree的性能

    • AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这
      样可以保证查询时高效的时间复杂度,即 l o g 2 ( N ) log_2 (N) log2(N)
    • 但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。
    • 因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合
  • 相关阅读:
    软考高项第四版教材整合管理(第8章)重点内容
    flutter doctor --android-licenses报错解决方案
    手把手带你使用JWT实现单点登录
    Android 组件化 组件上下依赖关系实现
    细说tcpdump的妙用
    Notepad++常用设置快捷键
    为什么和线程有关的方法会被封装在Object类中
    与“客户”沟通技巧
    【产品设计】有了创意,如何从零开始搭建一套产品模型
    spring boot启动报错java.lang.UnsupportedOperationException
  • 原文地址:https://blog.csdn.net/weixin_59400943/article/details/126268759