当前位置: 首页 > article >正文

【进阶数据结构】平衡搜索二叉树 —— AVL树

🌈感谢阅读East-sunrise学习分享——[进阶数据结构]AVL树
博主水平有限,如有差错,欢迎斧正🙏感谢有你 码字不易,若有收获,期待你的点赞关注💙我们一起进步🚀


🌈我们上一篇博客分享了搜索二叉树,在文中也铺垫了搜索二叉树的一些结构局限性
而今天分享的一种特殊的搜索二叉树——AVL树,便是一种结构优异的搜索二叉树🎄那么我们就开始吧🚀🚀🚀

目录

  • 一、AVL树的概念
  • 二、AVL树结点的定义
  • 三、AVL树的插入
  • 四、AVL树的旋转
    • 1.左单旋
    • 2.右单旋
    • 3.左右双旋
    • 4.右左双旋
  • 五、最终代码展示

一、AVL树的概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下

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

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

  • 它的左右子树都是AVL树
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hDG6AbRQ-1679390790392)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320143958347.png)]

这里的平衡因子是指:右子树高度-左子树高度

注意:平衡因子只是博主分享的这种实现方法的一种自定义名字(不是必须的),除了使用平衡因子之外还有许多实现AVL树的方法

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


二、AVL树结点的定义

AVL树的结点我们定义了一个三叉链结构,便于后续的操作;并且在每个结点中都引入了平衡因子

template<class K, class V>
struct AVLTreeNode
{
    //存储键值对的pair类
	pair<K, V> _kv;
    
    //含有父节点的三叉链
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;

    //平衡因子
	int _bf;

	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}
};

//AVL树
template<class K,class V>
struct AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
    //插入
    bool Insert(const pair<K, V>& kv)
    {}
    
private:
	Node* _root = nullptr;
};

三、AVL树的插入

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

  1. 按照二叉搜索树的方式插入新节点
  2. 调整平衡因子,若不平衡则需要旋转调整AVL树

⭕⭕当有新节点插入后我们就需要判断此时的树是否仍然平衡仍然是AVL树了


🚩插入后平衡因子的变化类型?

我们知道,假如平衡,则每个结点的平衡因子只有三种可能:-1,0,1

而插入新结点肯定会使得高度的变化,假如插入新节点后仍平衡,则父节点的平衡因子的变化有:

  • 0 --> 1
  • 0 --> -1
  • 1 --> 0
  • -1 --> 0

知道了平衡因子的变化情况后,又抛出了一个问题

🚩插入新节点影响父节点的平衡因子,那是否会影响祖先结点的平衡因子?

最简单的情况就是插入了新节点,只影响了其父结点,只需更新父节点的平衡因子

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yGlce0wT-1679390790393)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320151927976.png)]

插入新节点后,改变了其父结点(8)的子树高度,所以需要更新父节点的平衡因子,但是插入之后并不会改变其祖先结点的子树高度,所以不需要往上更新平衡因子

📌因此我们可以总结出:是否持续更新平衡因子,取决于其结点的子树高度是否变化

再结合一开始的平衡因子变化情况我们可以得出插入新结点后:

  • parent -> _bf == 0 —— 说明之前parent -> _bf 是 1 或者 -1(一边高一边低)新节点刚好插入填上矮的那边,parent所在子树高度不变 —— 祖先的子树高度也不会变 —— 只需更新parent的平衡因子,不需要继续往上更新
  • parent -> _bf == 1 或 -1 —— 说明之前parent -> _bf == 0(两边一样高)新结点插入使得parent所在子树的高度变得一高一低 —— 祖先的子树高度也产生变化 —— 更新parent的平衡因子之外,还需要继续往上更新祖先结点的平衡因子
  • parent -> _bf == 2 或 -2 —— 说明本就一高一低的子树,插入新节点后造成更加不平衡,此时违反了AVL树的平衡规则 —— 就地处理 ——旋转调整

最坏的情况就是插入了新节点,直接影响到了root根结点,所以需要持续更新到root根结点的平衡因子

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-inVjVnIr-1679390790394)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320154047198.png)]

💭更新结点的平衡因子时,假若我们需要持续向上更新平衡因子,一开始我们更新的是最下面的parent结点,更新后则可向上迭代,直到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 (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.first < kv.first)
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = 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)
			{
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == -2 || parent->_bf == 2)
			{
				//旋转调整
			}
			else
				assert(false);
		}
		return true;
	}

四、AVL树的旋转

🌏如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。

因此旋转的要求即是:

  1. 旋转后仍保持二叉搜索树的结构
  2. 旋转后整棵树保持平衡,平衡因子不超过1

而根据节点插入位置的不同,AVL树的旋转分为四种:

1.左单旋

1️⃣新节点插入较高右子树的右侧 —— 左单旋

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ewTMX0mf-1679390790394)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320191813189.png)]

此处我们给出左单旋过程的抽象图📌

我们发现,当parent的平衡因子是2,cur是1时,便进行左单旋 ——> 将cur的左子树给parent的右子树,然后将parent及其子树一整棵树变为cur的左子树

左单旋真就如此吗?不信我们可以画出具象图看看

当 h = 0

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JOtMtrcS-1679390790394)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320192229670.png)]

当 h = 1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TNDYGHPL-1679390790394)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320192802162.png)]

当 h = 2

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o56wtp0U-1679390790395)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320193527342.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ndiplxh7-1679390790395)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320194200461.png)]

💭有的兄弟看到这就有疑问,为什么h = 2时,子树c一定就得是z的模样呢?
因为假如子树c是x或y的模样,插入新节点时并不会引发节点30的旋转,那样最多只是变成以节点60为parent的树进行左单旋,那就和h = 1是同样的情况了💤因此以上的情况,其实是笼盖了所有需要进行左单旋的子情况了🚩然后以上的情况可能是某棵树的子树

最后我们发现,所有需要进行左单旋的情况,最后的操作都是如一开始所说

✏️代码实现(对照图更清晰易懂)

void RotateL(Node* parent)
{
	Node* subR = parent->_right;//parent的右孩子
	Node* subRL = subR->_left;//parent的右孩子的左孩子
		
    //旋转后subR的左孩子作为parent的右孩子
    parent->_right = subRL;

    //subR的左孩子有可能为空也有可能存在
    //如果存在则需要更新父子关系
	if (subRL)
		subRL->_parent = parent;

    //subR的左孩子变为以parent为根的子树结构
    //同时更新父子关系
    subR->_left = parent;
    parent->_parent = subR;
    
    //parent也可能只是一棵子树的根,其pparent可能为空也可能存在
	Node* pparent = parent->_parent;
	
	if (pparent)
	{
        //如果pparent不为空,则说明parent是一棵子树
        //可能是存在于其父节点的左子树or右子树
		if (parent == pparent->_left)
			pparent->_left = subR;
		else
			pparent->_right = subR;
		subR->_parent = pparent;
	}
	else
	{
        //若pparent为空,则说明parent是整棵树的根节点
        //旋转后根节点已经换人了需要更新
		_root = subR;
		subR->_parent = nullptr;
	}
    //最后更新平衡因子
	parent->_bf = subR->_bf = 0;
}

📌看完以上的代码实现,发现旋转的代码实现起来也有许多细节需要注意啊…

因为旋转后也要保持一棵正常的树的结构,因此那些父子链接关系也需要正确更新

2.右单旋

2️⃣新节点插入较高左子树的左侧 - 右单旋

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0rczBlb6-1679390790395)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320224253056.png)]

✏️实现及情况考虑可参考左单旋

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 (pparent)
	{
		if (parent == pparent->_left)
			pparent->_left = subL;
		else
			pparent->_right = subL;
		subL->_parent = pparent;
	}
	else
	{
		_root = subL;
		subL->_parent = nullptr;
	}
	subL->_bf = parent->_bf = 0;
}

3.左右双旋

3️⃣新节点插入较高左子树的右侧 - 先左单旋再右单旋

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3Mn250NT-1679390790395)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320225421511.png)]

左右双旋我们可以复用上面的左单旋和右单旋的代码🚩但是需要注意的是,左右双旋完各个节点的平衡因子有不同的情况,正是因为左右双旋会因为新节点插入的位置不同而影响不同的旋转结果,因此我们总结出了以下三种情况:

  1. h = 0 —— 节点60即是新插入节点
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fkFAFXcU-1679390790396)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320230931222.png)]

  2. 新节点插入在b
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1pptemfj-1679390790396)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320231609130.png)]

  3. 新节点插入在c
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JGZNUPir-1679390790396)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320231628170.png)]

综上所述,当我们在实现左右双旋时的最后,可根据插入新节点后节点60的平衡因子大小,来确定不同的情况

void RotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	int bf = subLR->_bf;

	RotateL(parent->_left);
	RotateR(parent);
    
    //更新平衡因子

	if (bf == 1) //新增在sublr右子树
	{
		parent->_bf = 0;
		subL->_bf = -1;
		subLR->_bf = 0;
	}

	else if (bf == -1) //新增在sublr左子树
	{
		subL->_bf = 0;
		parent->_bf = 1;
		subLR->_bf = 0;
	}

	else //本身就是新增
	{
		parent->_bf = 0;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
}

4.右左双旋

4️⃣新节点插入较高右子树的左侧——先右单旋再左单旋

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RGitbiDD-1679390790396)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320232205457.png)]

✏️实现及情况考虑可参考左右双旋

void RotateRL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	int bf = subRL->_bf;

	RotateR(parent->_right);
	RotateL(parent);

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

五、最终代码展示

template<class K, class V>
struct AVLTreeNode
{
	pair<K, V> _kv;
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;

	int _bf;

	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}
};

template<class K,class V>
struct AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:

	AVLTree()
		:_root(nullptr)
	{}

	

	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 = new Node(kv);
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = 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)
			{
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == -2 || parent->_bf == 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;
	}

	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;

		if (pparent)
		{
			if (parent == pparent->_left)
				pparent->_left = subR;
			else
				pparent->_right = subR;
			subR->_parent = pparent;
		}
		else
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		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 (pparent)
		{
			if (parent == pparent->_left)
				pparent->_left = subL;
			else
				pparent->_right = subL;
			subL->_parent = pparent;
		}
		else
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		subL->_bf = parent->_bf = 0;
	}

	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;

		RotateL(parent->_left);
		RotateR(parent);

		if (bf == 1) //新增在sublr右子树
		{
			parent->_bf = 0;
			subL->_bf = -1;
			subLR->_bf = 0;
		}

		else if (bf == -1) //新增在sublr左子树
		{
			subL->_bf = 0;
			parent->_bf = 1;
			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 == 1)
		{
			subR->_bf = 0;
			parent->_bf = -1;
			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);
		}
	}

	void Inorder()
	{
		_Inorder(_root);
	}
	
	void _Inorder(Node* root)
	{
		if (root == nullptr)
			return;

		_Inorder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_Inorder(root->_right);
	}

	int Height(Node* root)
	{
		if (root == nullptr)
			return 0;

		int hl = Height(root->_left);
		int hr = Height(root->_right);

		return hl > hr ? hl + 1 : hr + 1;
	}

	bool IsBalance()
	{
		return IsBalance(_root);
	}

	bool IsBalance(Node* root)
	{
		if (root == nullptr)
			return true;

		int leftHeight = Height(root->_left);
		int rightHeight = Height(root->_right);

		if (rightHeight - leftHeight != root->_bf)
		{
			cout << "平衡因子异常" << endl;
			return false;
		}

		return abs(rightHeight - leftHeight) < 2
			&& IsBalance(root->_left)
			&& IsBalance(root->_right);
	}

private:
	Node* _root = nullptr;
};

🌈🌈写在最后 我们今天的学习分享之旅就到此结束了
🎈感谢能耐心地阅读到此
🎈码字不易,感谢三连
🎈关注博主,我们一起学习、一起进步


http://www.kler.cn/a/2267.html

相关文章:

  • 如何解决vscode powershell乱码
  • scala基础学习(数据类型)-字符串
  • 江苏计算机专转本 技能Mysql知识点总结(二)
  • Pytorch | 从零构建AlexNet对CIFAR10进行分类
  • 面试题整理9----谈谈对k8s的理解1
  • 设计模式之【观察者模式】
  • DRAM功能介绍与基础概念
  • Android Navigation的四大要点你都知道吗?
  • 操作系统(2.4.5)--管程机制
  • String类为什么被设计成final,这样设计有什么好处
  • 【C语言】你真的了解结构体吗
  • linux系统运维面试题大全(137道题)
  • 博客项目
  • Python中的微型巨人-Flask
  • Spark - 继承 FileOutputFormat 实现向 HDFS 地址追加文件
  • Linux- 系统随你玩之--玩出花活的命令浏览器-双生姐妹花
  • 基于ssm大学生竞赛活动平台(包含万字文档)020
  • 【java】笔试强训Day1
  • 膜拜!阿里自爆十万字Java面试手抄本,脉脉一周狂转50w/次
  • 自定义类型的超详细讲解ᵎᵎ了解结构体和位段这一篇文章就够了ᵎ
  • 蚁群算法c++
  • 【前端八股文】浏览器系列:性能优化——HTML、CSS、JS、渲染优化
  • 【Java集合】Collection接口中的常用方法
  • RK3588平台开发系列讲解(显示篇)DP显示调试方法
  • 关于STM32用DMA传输UART空闲中断中接收的数据时无法接收数据问题以及解决办法
  • STL库中list的迭代器实现痛点分析