初识AVL树

这篇文章将主要将AVL树的插入

一.AVL树相关概念

1.AVL树是一颗空树或具有以下性质的二叉搜索树

它的左右子树都是AVL树;

左右子树的高度差不超过1;

这些性质使AVL树是一颗高度平衡的二叉搜索树,可以通过控制其高度差去控制平衡

2.在AVL树引入一个平衡因子(balance factor) 的概念,每个节点都有一个平衡因子,它的值是该节点右子树的高度减去左子树的高度 (也可以是左子树减右子树,这篇文章统一右子树减左子树),也就是说正常平衡因子的值只有-1/0/1三种情况。AVL树不一定要平衡因子,这只是AVL树的其中一种实现方式,通过它我们可以更方便的观察树是否平衡。

3.AVL树的节点分布与完全二叉树类似,高度可控制到logN,极大提高的增删查改的效率,相比于二叉搜索树有了质的提升。

二.AVL树的实现

1.AVL树的结构

相比于二叉搜索树,AVL树的节点多了parent指针和平衡因子:

cpp 复制代码
template<class k,class v>
struct TreeNode
{
	//平衡因子和parent可以没有,但这要用另一种方法实现了

	TreeNode* _left;
	TreeNode* _right;
	TreeNode* _parent;
	pair<k, v> _kv;//储存的数据
	int _bf;//blance factor,平衡因子
	TreeNode(const pair<k, v>& data = pair<k, v>())
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
		,_kv(data)
	{ }
};

树本体的成员变量有个根就好了:

cpp 复制代码
template<class k, class v>
class AVLTree
{
	typedef TreeNode<k, v> Node;
public:
private:
	Node* _root = nullptr;
};
2.AVL树的插入
1.大概过程

1.按照二叉搜索树的插入逻辑插入节点;

2.插入节点后可能会影响该节点祖先节点的高度,也就是说会影响它们的平衡因子,所以要更新新插入节点->根节点路径上的节点的平衡因子,最坏要更新到根节点,有些情况更新到中途即可停止;

3.更新平衡因子的过程中没有出现问题(所有节点的平衡因子为-1/0/1),则插入结束;

4.更新平衡因子的过程中出现问题(有节点的平衡因子不为-1/0/1),则要对以出现问题的节点为根的子树进行旋转操作,该操作本质上是在调整平衡的过程中降低该子树的高度,不会影响上面的节点,所以插入结束。

2.平衡因子的更新

更新原则:

1.平衡因子 = 左子树高度 - 左子树高度;

2.只有子树高度变化的才会影响当前节点平衡因子;

3.如果节点插入在parent的右子树,这parent的平衡因子++,在左子树则--;

4.以parent为根节点地子树高度是否变化决定了是否需要向上更新平衡因子;

更新停止条件:

1.更新后parent的平衡因子为0 ,也就是说更新过程中parent的平衡因子变化为-1->0或1->0,即原来该子树是一边高一边低,插入新节点后该子树就平衡了,不会影响parent的父节点的平衡因子,更新结束

2.更新后parent的平衡因子为1/-1 ,也就是说更新过程中parent的平衡因子变化为0->1或着0->-1,即原来该子树是两边高度相同的,插入新节点后该子树的高度必发生改变,因此必影响parent父节点的平衡因子,因此要继续往上更新

3.更新后parent的平衡因子为2/-2 ,这时候就表示着该节点的平衡因子出问题了,要对以该节点为根节点的子树进行旋转操作

4.一直更新到根节点,且根节点的平衡因子没有问题,也代表着更新结束

cpp 复制代码
	bool insert(const pair<k, v>& data)
	{
		//普通二叉搜索树的插入逻辑
		if (_root == nullptr)
		{
			_root = new Node(data);
			return true;
		}
		
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (data.first > cur->_kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (data.first < cur->_kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
				return false;
		}
		cur = new Node(data);
		cur->_parent = parent;
		if (data.first > parent->_kv.first)
			parent->_right = cur;
		else
			parent->_left = cur;

	//调整平衡因子
		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)
			{
				//旋转
				break;
			}
			else
			{
			//如果出现意料外的结构,则给个报错
				assert(false);
			}
		}
			
		return true;
	}
3.旋转
1.旋转的原则

1.旋转后的树仍要保存搜索树的规则;

2.让旋转的树由不平衡变平衡,其次降低树的高度

旋转分为4中:

左、右单旋,左右、右左双旋。

2.左、右单旋

首先我们要知道,插入元素后要进行旋转的子树 在插入元素前(平衡因子为±1)是长这样的:

对于左边这棵树,只有将元素插入到子树b/c中,才要对以5为根的子树进行旋转;

对于右边这棵树,只有将元素插入到子树a/b中,才要对以10为根的子树进行旋转。

a,b,c的高度>=0。

虽然上面可以让子树发生旋转的选择有两个,当如果只是要进行单旋,只有两种情况:

左边的树:插入在c子树;

右边的树:插入到a子树;

旋转的方法(拿左边的树举例):

虽然看起来逻辑比较简单,当我们需要改动很多节点的各成员指向,比如节点5的parent、right等,这是比较麻烦的。最容易忽略的是,一开始节点5可能是由parent节点的,也可能它就是一整颗树的根节点。

cpp 复制代码
	void RotateL(Node* parent)//左单旋
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		Node* Pparent = parent->_parent;

		parent->_right = subRL;//将subR的左子树给parent当右子树
		subR->_left = parent;//将原来的parent给subL当左子树
		if (subRL)
			subRL->_parent = parent;//更新subR左子树的父节点,要注意其是否为空
		if (parent == _root)
			_root = subR;
		subR->_parent = Pparent;
		if (Pparent && parent == Pparent->_left)
			Pparent->_left = subR;
		else if(Pparent)
			Pparent->_right = subR;
		parent->_parent = subR;//将该子树的根节点改为subR;
		subR->_bf = 0;
		parent->_bf = 0;
	}

右单旋也是类似的,不过它是把parent的left的right给parent的left。

cpp 复制代码
	void RotateR(Node* parent)//右单旋
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		Node* Pparent = parent->_parent;

		parent->_left = subLR;//将subL的右子树给parent当左子树
		subL->_right = parent;//将原来的parent给subL当右子树
		if (subLR)
			subLR->_parent = parent;//更新subL右子树的父节点,要注意其是否为空
		if (parent == _root)
			_root = subL;
		subL->_parent = Pparent;
		if (Pparent && Pparent->_left == parent)
			Pparent->_left = subL;
		else if(Pparent)
			Pparent->_right = subL;
		parent->_parent = subL;//将该子树的根节点改为subL;
		subL->_bf = 0;
		parent->_bf = 0;
	}
3.左右、右左双旋

对于元素插入位置的选择,上面提到的另外两种情况就要对子树进行双旋:

对于左边的树,元素插入到b树需要对该树进行双旋;

对于右边的树,元素插入到b树需要对该树进行双旋。

那么双旋该怎么做呢?

拿左边的树举例(右左双旋 )说:

简单来说就是对于一开始右边高的子树,先对parent的右子树进行右旋,再对parent进行左旋;对于一开始左边高的树,先对parent的左子树进行左旋,再对parent进行右旋

但麻烦的还是接下来的更新平衡因子,对于双旋还会分三种情况。

继续拿左边的树举例,在插入新元素前:

parent和subR和subRL是关键的三个节点,因为只会以它们为根调整树,也就是说在这棵树中,只有它们的平衡因子会改变。

1.对于h == 0

旋转结束后三个关键节点的平衡因子都变为了0;

2.对于h >= 1 且 元素插入在b1

旋转后:

parent:0

subR:1

subRL:0

2.对于h >= 1 且 元素插入在b2

旋转后:

parent:-1

subR:0

subRL:0

因此我们可以写出右左双旋的代码:

cpp 复制代码
	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;
		//只有它们两和parent的平衡因子要改变
		
		//subRL的右子树给subR的左
		//subRL的左子树给parent的右
		RotateR(parent->_right);//先将父节点的右子树进行右旋,让以parent为根的
		//树变成只需单旋的情况
		RotateL(parent);//再将该树进行左单旋

		//最后要修改平衡因子
		if (bf == 0)//此时树中只有两个节点
		{
			parent->_bf = 0;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == 1)//此时新插入的节点再subRL的右子树
		{
			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);
	}

左右双旋的代码也是同理:

cpp 复制代码
	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		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 = 0;
			subL->_bf = -1;
			subLR->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 1;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else
			assert(false);
	}
4.插入代码
cpp 复制代码
class AVLTree
{
	typedef TreeNode<k, v> Node;
public:
	bool insert(const pair<k, v>& data)
	{
		//普通二叉搜索树的插入逻辑
		if (_root == nullptr)
		{
			_root = new Node(data);
			return true;
		}
		
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (data.first > cur->_kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (data.first < cur->_kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
				return false;
		}
		cur = new Node(data);
		cur->_parent = parent;
		if (data.first > parent->_kv.first)
			parent->_right = cur;
		else
			parent->_left = cur;

	//调整平衡因子
		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)//右单旋
				{
					RotateR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == 1)//左单旋
				{
					RotateL(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == -1)//右左双旋
				{
					RotateRL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == 1)//左右双旋
				{
					RotateLR(parent);
				}
				break;
			}
			else
			{
				assert(false);
			}
		}
			
		return true;
	}
	void RotateR(Node* parent)//右单旋
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		Node* Pparent = parent->_parent;

		parent->_left = subLR;//将subL的右子树给parent当左子树
		subL->_right = parent;//将原来的parent给subL当右子树
		if (subLR)
			subLR->_parent = parent;//更新subL右子树的父节点,要注意其是否为空
		if (parent == _root)
			_root = subL;
		subL->_parent = Pparent;
		if (Pparent && Pparent->_left == parent)
			Pparent->_left = subL;
		else if(Pparent)
			Pparent->_right = subL;
		parent->_parent = subL;//将该子树的根节点改为subL;
		subL->_bf = 0;
		parent->_bf = 0;
	}
	void RotateL(Node* parent)//左单旋
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		Node* Pparent = parent->_parent;

		parent->_right = subRL;//将subR的左子树给parent当右子树
		subR->_left = parent;//将原来的parent给subL当左子树
		if (subRL)
			subRL->_parent = parent;//更新subR左子树的父节点,要注意其是否为空
		if (parent == _root)
			_root = subR;
		subR->_parent = Pparent;
		if (Pparent && parent == Pparent->_left)
			Pparent->_left = subR;
		else if(Pparent)
			Pparent->_right = subR;
		parent->_parent = subR;//将该子树的根节点改为subR;
		subR->_bf = 0;
		parent->_bf = 0;
	}
	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;
		//只有它们两和parent的平衡因子要改变

		//subRL的右子树给subR的左
		//subRL的左子树给parent的右
		RotateR(parent->_right);//先将父节点的右子树进行右旋,让以parent为根的
		//树变成只需单旋的情况
		RotateL(parent);//再将该树进行左单旋

		//最后要修改平衡因子
		if (bf == 0)//此时树中只有两个节点
		{
			parent->_bf = 0;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == 1)//此时新插入的节点再subRL的右子树
		{
			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);
	}
	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		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 = 0;
			subL->_bf = -1;
			subLR->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 1;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else
			assert(false);
	}
private:
	Node* _root = nullptr;
};
相关推荐
小小de风呀1 小时前
de风——【从零开始学C++】(七):string类详解
开发语言·c++·算法
江屿风1 小时前
【c++笔记】类和对象流食般投喂(中)
开发语言·c++·笔记
Huangjin007_1 小时前
【C++ STL篇(八)】set容器——零基础入门与核心用法精讲
开发语言·c++·学习
许长安1 小时前
Kafka 架构讲解:从提交日志到分区副本机制
c++·经验分享·笔记·分布式·架构·kafka
邪修king1 小时前
UE5 TA 核心修炼:材质与纹理艺术全解 —— 从 PBR 理论到工业级材质实战
c++·后端·游戏·ue5·材质
hehelm1 小时前
C++ 特殊类设计
开发语言·c++
智者知已应修善业11 小时前
【51单片机89C51及74LS273、74LS244组成】2022-5-28
c++·经验分享·笔记·算法·51单片机
Byron Loong13 小时前
【c++】为什么有了dll和.h,还需要包含lib
java·开发语言·c++
坚果派·白晓明14 小时前
【鸿蒙PC三方库移植适配框架解读系列】第一篇:Lycium C/C++ 三方库适配 — 概述与环境配置
c语言·开发语言·c++·harmonyos·开源鸿蒙·三方库·c/c++三方库