• [数据结构]AVL树



    上文对map/multimap/set/multiset进行了简单的介绍,这几个容器有个共同点是:
    其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现


    一、AVL树

    1.1 AVL树的概念

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

    AVL树也叫作高度平衡二叉树(红黑树是控制颜色)

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

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

    AVL树不一定需要平衡因子,使用平衡因子是一种控制实现的方式

    如果一棵二叉搜索树是高度平衡的,他就是AVL树。如果他有N个节点,其高度可以保持在O(logN),搜索时间复杂度为O(logN)

    1.2 AVL树基本框架

    // AVLTree节点的定义
    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)
    		,_bf(0)
    		,_kv(kv)
    };
    
    // AVLTree的定义
    template<class K, class V>
    class AVLTree
    {
    	typedef AVLTreeNode<K, V> Node;
    private:
    	AVLTree()
    		:_root(nullptr)
    	{}
    
    private:
    	Node* _root;
    };
    
    • 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

    1.3 AVL树的插入

    在这里插入图片描述

    在AVL树中,我们采用平衡因子来实现,如果插入在父节点的右边,parent的平衡因子++,如果在左边则–

    那么cur的增加会影响parent,那会影响祖先吗?——会,只可能影响祖先

    cur新增节点只会影响cur祖先节点的平衡因子

    在这里插入图片描述

    1. parent -> left == cur , parent -> bf--
    2. parent -> right == cur , parent -> bf++
    3. 更新后,如果parent -> bf == 0 则更新结束,说明更新前parent->bf是1或是-1,现在变为了0则表明原先是不平衡的状态,现在是平衡的,parent所在的子树高度不变
    4. 更新后,如果parent -> bf == 1 / -1 则继续往上更新,说明更新前parent->bf是0,现在变为了1 / -1,则说明我有一边的子树变高了,parent所在的子树高度也就变了
    5. 更新后,如果parent -> bf == 2 / -2,则说明parent子树已经不平衡了,需要进行旋转处理
    
    • 1
    • 2
    • 3
    • 4
    • 5

    演示三种不同的情况:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    template<class K, class V>
    class AVLTree
    {
    	typedef AVLTreeNode<K, V> Node;
    public:
    	AVLTree()
    		:_root(nullptr)
    	{}
    	
    	bool Insert(const pair<K, V>& kv)
    	{
    		if (_root == nullptr)
    		{
    			_root = new Node(kv);
    		}
    
    		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 < kv.first)
    		{
    			parent->_right = cur;
    			cur->_parent = parent; // 这里是三叉链
    		}
    		else // 这里不可能相等,因为相等上面就走false了
    		{
    			parent->_left = cur;
    			cur->_parent = parent;
    		}
    	
    		// 前面是插入,下面控制平衡
    		/* 一个节点的平衡因子受到子树的影响,新插入节点的平衡因子一定为0(子树为空树)
    		如果新节点插入在父亲的右边,则平衡因子++
    		如果新节点插入在父亲的左边,则平衡因子--
    		控制平衡有两步
    		1. 更新平衡因子—更新新增节点到根节点的祖先路径
    		2. 如果出现异常的平衡因子,就需要旋转平衡处理
    		*/
    	}
    
    private:
    	Node* _root;
    };
    
    • 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
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    1.4 AVL树的旋转

    旋转的意义:

    1. 保持搜索树规则
    2. 控制平衡

    通俗来说也就是使得这棵树平衡了,这棵树的高度降了1

    while (parent)
    {
    	if (cur == parent->_left) // 若插入在左侧,则parent的平衡因子--
    	{
    		parent->_bf--;
    	}
    	else
    	{
    		parent->_bf++;
    	}
    
    	// 更新完平衡因子后判断是否需要继续进行处理
    
    	if (parent->_bf == 0)
    	{
    		break;
    	}
    	else if (parent->_bf == 1 || parent->_bf == -1)
    	{
    		cur = parent;
    		parent = parent->_parent;
    	}
    	else if (parent->_bf == 2 || parent->_bf == -2)
    	{
    		// 旋转
    		if (parent->_bf == -2 && cur->_bf == -1)
    		{
    			RotateR(parent);
    		}
    		else if (parent->_bf == 2 && cur->_bf == 1) // 左单旋
    		{
    			RotateL(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);
    	}
    }
    
    • 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
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    1.4.1 右单旋

    下图中a、b、c是高度为h的平衡搜索树
    在这里插入图片描述
    用抽象图可以代表各种右旋的情况,旋转的操作:

    1. b变为60的左边
    2. 60变成30的右边
      在这里插入图片描述

    上图实际表示出来就是下面这样子
    在这里插入图片描述

    因此就可以实现右旋代码,如下图所示可以用代码实现其连接关系
    在这里插入图片描述

    void RotateL(Node* parent)
    {
    	Node* subL = parent->_left;
    	Node* subLR = subL->_right;
    
    	parent->_left = subLR;
    	if (subLR) // subLR可能会是空
    		subLR->_parent = parent; // 因为是三叉链,还需要去更新parent
    
    	Node* parentParent = parent->_parent; // 先存储祖宗
    
    	subL->_right = parent;
    	parent->_parent = subL;
    
    	// 没有祖先
    	if (parent == _root)
    	{
    		_root = subL;
    		_root->_parent = nullptr;
    	}
    	else // 还有祖先,将祖先的左右指针指向刚刚更新后子树的新的root
    	{
    		if (parentParent->_left == parent)
    		{
    			parentParent->_left = subL;
    		}
    		else
    		{
    			parentParent->_right = subL;
    		}
    		subL->_parent = parent;
    	}
    
    	parent->_bf = 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

    1.4.2 左单旋

    右侧高了,因此我们使用左单旋
    在这里插入图片描述

    void RotateL(Node* parent)
    {
    	Node* subR = parent->_right;
    	Node* subRL = subR->_left;
    
    	parent->_right = subRL;
    	if (subRL) // subRL可能会是空
    		subRL->_parent = parent;
    
    	Node* parentParent = parent->_parent;
    
    	subR->_left = parent;
    	parent->_parent = subR;
    
    	if (_root == parent)
    	{
    		_root = subR;
    		subR->_parent = nullptr;
    	}
    	else
    	{
    		// 如果有祖先
    		if (parentParent->_left == parent)
    		{
    			parentParent->_left = subR;
    		}
    		else
    		{
    			parentParent->_right = subR;
    		}
    		subR->_parent = parentParent;
    	}
    
    	subR->_bf = parent->_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

    1.4.3 左右双旋

    新节点插入较高左子树的右侧:左右 -> 先左单旋在进行右单旋
    在这里插入图片描述
    在这里插入图片描述

    1.4.4 右左双旋

    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;
    		subR->_bf = 0;
    		subRL->_bf = 0;
    	}
    	else if (bf == -1)
    	{
    		parent->_bf = 0;
    		subR->_bf = 1;
    		subRL->_bf = 0;
    	}
    	else if (bf == 0)
    	{
    		parent->_bf = 0;
    		subR->_bf = 0;
    		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

    双旋旋转完后要记得去更新平衡因子

  • 相关阅读:
    对给定的数组进行重新排列numpy.random.shuffle()和numpy.random.permutation()
    云原生之深入解析Kubernetes集群内的服务通信机制
    liteos连接器脚本隐藏的指针问题
    Stable Diffusion 动画animatediff-cli-prompt-travel
    Java基于PHP+MySQL客户信息管理系统的设计与实现
    使用 AI 编程助手 CodeWhisperer,开发如有神助
    Code For Better 谷歌开发者之声——TensorFlow
    【QT】Windows 编译并使用 QT 5.12.7源码
    R语言电信公司churn数据客户流失 k近邻(knn)模型预测分析
    盘点世界杯有趣小知识!带你感受体育赛事数据可视化的快乐!
  • 原文地址:https://blog.csdn.net/weixin_51304981/article/details/126262081