C++ AVL树的学习

目录

1.AVL树的概念

2.AVL树的结构

3.AVL树的插入

3.1平衡因子的更新

4.插入代码的实现

5.旋转

5.1.右旋

5.2左旋

5.3左右旋

5.4右左旋

6.旋转的代码实现

7.AVL树的平衡检验


之前我们学习了二叉搜索树,注意到二叉搜索树在最坏情况下会退化成单支树,查找效率会大幅度下降,而AVL树解决了这个问题,它保证整棵树的查找效率始终为O(logN)

1.AVL树的概念

AVL树是一颗平衡二叉树,从名字可以看出,AVL树的左右子树的高度肯定不会相差很大,不然就不能称之为平衡,所以引出AVL树的性质,对于一个节点来说,它的左右子树高度差不会超过1,且它的左右子树均继承这个性质,从而保证了整棵树左右相对平衡,这里我们引入一个平衡因子的概念,每个节点都包含一个平衡因子,平衡因子的值等于右子树高度减去左子树高度,且取值范围为-1,0,1这三个值,不过AVL树不一定需要平衡因子,只不过有了平衡因子控制和观察树是否平衡会变得更简便

那么为什么高度差不控制为0而是不超过1,假如现在有一棵满二叉树,它每一层的节点个数都满了,如果此时再插入一个节点,那么对于根节点左右子树的高度差一定会发生变化,所以高度差是不可能严格控制到0的,但是保持在0或者1是可以做到的

2.AVL树的结构

pair的作用就是将一组值耦合在一起,形成一个组合,没有其他特殊含义,将key和value作为一份数据存储在节点中,使用更加严谨和方便

之前学过的二叉搜索树中,每个节点只有key,这里只是多了一个value作为它实际存储的值,依旧是使用key来进行比较遍历,确定每个节点插入的位置,只需把key当作重点即可

而对于AVL树的每个节点除了左右节点,还新增了父亲节点,这样就构成了双向的操作,对于一个节点,可以直接查找到它的父亲节点,在迭代器进行遍历的时候就会很方便,AVL树的迭代器部分后面在讲

cpp 复制代码
//AVL树的每个节点,定义为一个结构体
template<class K,class V>
struct AVLTreeNode {

	pair<K, V> _kv;//将一对值耦合在一起,分别是key和value
	//定义三个节点,对应左右节点和父亲节点
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;

	int _bf = 0;//平衡因子,范围是0~2

	//初始化列表,将本节点和平衡因子进行初始化
	//其余节点置为nullptr,后续进行赋值
	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}

};

3.AVL树的插入

首先先按照二叉搜索树的规则进行插入,然后就要通过平衡因子进行判断是否出现不平衡的情况,并且新插入节点后,对于它的部分祖先节点的平衡因子会产生影响,要向上进行更新

例如在插入X节点之前,A,B,C的平衡因子均为0,但是在C右侧插入节点后,C的平衡因子变为1,B平衡因子变为1,C的平衡因子变为-1,这种情况就是对所有祖先节点的平衡因子都产生了影响,但是如果原来在最左侧还有一个节点,那么插入X节点时就不会对A的平衡因子产生影响

但是此时,如果在X的左右节点插入一个新的节点,此时就会出现不平衡,因为C的平衡因子会变成2,破坏了AVL树左右子树不高度差不超过1的规则,所以要使用旋转的操作进行调整,在旋转之后当前子树会变得平衡,高度和插入前保持一致,不会影响祖先节点,不用继续向上更新,对于旋转的具体操作后续会讲,这里先知道平衡因子异常需要进行旋转

3.1平衡因子的更新

更新平衡因子有几条原则

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

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

3.对于某节点来说,在他的左右插入新的节点,如果插入在右子树那么平衡因子++,插入在左子树平衡因子--,注意新插入的节点和某节点是直接连接的,也就是父子节点才能这样更新

4.某节点所在子树高度是否发生变化,决定是否需要继续向上更新平衡因子

更新结束的条件:

我这里将某子树的根节点叫做node,方便讲解

注意这里更新后,node节点的平衡因子是发生变化的,如果和原来一样就没有讨论意义了

1.对于node节点更新后平衡因子为0,且更新过程中它的平衡因子是从1或者-1变化到0的,说明它的左右子树原来一边高,更新后左右子树平衡了,但是更新后node所在子树的整体高度并不会变化,例如原来左子树高,那么在右子树新插入一个节点使得右子树高度和左子树保持一致,整体高度依旧是左子树的高度,所以此时更新结束,不用向上更新

如图,节点A在插入红色节点前,平衡因子是1,插入节点后使得A的平衡因子为0,此时该子树高度未发生变化,不需要调整

2.更新后node节点的平衡因子为-1或者1,且原来的平衡因子为0,说明更新前node左右子树平衡,更新后node节点的左右子树一边高,node所在子树的平衡因子符合要求,但是高度发生了变化,需要继续向上调整

如图,对于A节点来说原本平衡因子为0,更新后左子树高,平衡因子变为-1,此时需要继续向上调整,对于节点B来说左子树变高,平衡因子从0变化到-1,所以最坏情况可能需要调整到根节点

3.更新后node节点平衡因子为-2或者2,且原来平衡因子为-1或者1,说明更新前左右子树有一边高,且新插入的节点插入在高的那一边,此时需要进行旋转的操作,将node所在子树旋转,使得该子树的高度和插入前保持一致,所以旋转后子树高度不变,不需要继续向上调整

如左图,此时插入节点后,使得A的平衡因子变为-2,需要进行旋转的操作,最后这棵树会变成右边这样,具体怎么执行旋转的操作后文单独讲解

4.插入代码的实现

ps:旋转代码在讲解完旋转操作后给出

那么按照刚才讲的插入逻辑,先查找对应位置,然后插入节点,之后调整平衡因子,判断是否合理,如果平衡因子异常则需要调整

cpp 复制代码
//插入节点
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->_left;
			
		}
		else if (cur->_kv.first < kv.first) {
			parent = cur;
			cur = cur->_right;
			
		}
		else return false;
	}
	//找到位置后,将cur新建节点,叶子节点平衡因子为0
	cur = new Node(kv);
	cur->_bf = 0;

	//确实是父亲节点的左边还是右边
	if (cur->_kv.first < kv.first) {
		parent->_left = cur;
	}
	else {
		parent->_right = cur;
	}
	cur->_parent = parent;
	//开始计算平衡因子,如果超出[-1,1]就要进行旋转
	while (parent) {
		if (cur == parent->_right) {
			parent->_bf++;
		}
		else {
			parent->_bf--;
		}
		//如果该节点原来是-1或者1,说明左右一边高
		//变成0之后平衡,该子树高度不变,不对祖先节点产生影响
		//直接跳出
		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 return false;

		}
	}

	return true;

}

5.旋转

旋转分为四种,左旋,右旋,左右旋,右左旋

5.1.右旋

如图情况,对于节点A来说,在B的左子树插入一个新节点后,A的左子树高度为h+2,右子树高度为h,所以需要进行旋转的操作,既然是右旋,那就是向右进行旋转

我们的目的是旋转之后高度和插入前保持一致,所以此时A节点不再是这棵子树的根节点,我们选择让B节点成为新的根节点,然后A成为B的右节点,但是这样B原来的右子树就没地方连接了,注意到A成为B的右节点前,要先断开左子树的连接,所以我们可以把B的右节点给到A的左节点

所以根据以上逻辑,让A的左节点B成为新的根节点,A成为B的右节点,然后将B的右子树作为A的左子树,以下是旋转之后的图,此时我们来验证一下是否符合搜索二叉树的规则,对于B的右子树来说,B是A的左子树,所以B的右子树一定小于A,放在A的左子树位置没问题,因为B比A小,所以A作为B的右子树也没问题,所以旋转不会破坏搜索二叉树的规则,后续就不再赘述

5.2左旋

左旋的操作就是和右旋相反

如图,新插入的节点在右侧,此时节点A的平衡因子为2,那么需要进行左旋的操作

逻辑就是和右旋相反,我们将B作为新的根节点,然后将B的左子树作为A的右子树,将A连接到B的左节点即可,以下为旋转之后的结果

还有一个注意事项,那就是这棵树可能也只是某个节点的子树,所以我们在旋转之前,要先记录这棵树的父亲节点,然后旋转完毕后,让父亲节点与新的根节点进行连接,后面的双旋也是一样

5.3左右旋

如图,对于A节点是左边子树高,对于B节点是右边子树高,那么如果只用一次上面学的单旋,无法达到目的(可以自己画图验证一下),这时候就需要进行两次旋转且方向相反

我们以B节点作为根节点,先进行一次左旋,然后此时整棵树只有左边子树高,那么再以A节点进行一次右旋,此时整棵树就达到了平衡

以下是旋转之后的图,可以简化逻辑,我们将C作为新的根节点,B和A作为C的左右节点,然后将C的左右子树分别给到B和A即可,当插入节点在C左侧时,该节点会分到B的右子树,当插入节点在C右侧时,该节点就会分到A的左子树,也就是图中红色h和h-1这两个位置

5.4右左旋

情况就是和左右旋相反,先进行一次右旋,再进行一次左旋、

以下是旋转之后的图

6.旋转的代码实现

对于右旋来说,我们需要找到当前节点parent,当前节点的左节点subL,subL的右节点subLR,以及当前节点的父节点pparent,因为这四个节点的连接发生了改变

左旋逻辑相反,按照这个思路反着找四个节点即可

cpp 复制代码
//右旋
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;
		subL->_parent = nullptr;
	}
	//否则判断原来子树的根节点是上一层父亲节点的做还是右,进行链接
	else {
		if (parent = pparent->_left) {
			pparent->_left = subL;
		}
		else {
			pparent->_right = subL;
		}
		subL->_parent = pparent;
	}
	//旋转后应该是平衡的,平衡因子为0
	parent->_bf = subL->_bf = 0;

}
//左旋
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 (parent == _root) {
		_root = subR;
		subR->_parent = nullptr;
	}
	//否则判断原来子树的根节点是上一层父亲节点的做还是右,进行链接
	else {
		if (parent = pparent->_left) {
			pparent->_left = subR;
		}
		else {
			pparent->_right = subR;
		}
		subR->_parent = pparent;
	}
	//旋转后应该是平衡的,平衡因子为0
	parent->_bf = subR->_bf = 0;
}

左右双旋,找到需要旋转的子树,找到对应节点复用左旋和右旋即可

因为刚才讲过,(按照旋转部分的图例)双旋转实际上对节点C的左右节点进行了分配,所以我们要判断新插入的节点在C节点的左侧还是右侧,这决定了AB节点的平衡因子

cpp 复制代码
//左右旋转代码
void RotateLR(Node* parent) {

	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	//本质是将subLR的左或者右节点分配给subL或者parent
	int bf = subLR->_bf;

	//对两个节点进行左旋右旋
	RotateL(subL);
	RotateR(parent);

	if (bf == 0) {
		//此时只有三个节点,旋转完成后全部平衡
		subL->_bf = 0;
		parent->_bf = 0;
		subLR->_bf = 0;
	}
	else if (bf == 1) {
		//subLR右边的节点会给到parent的左边
		//所以parent平衡,subL左边高
		subL->_bf = -1;
		parent->_bf = 0;
		subLR->_bf = 0;

	}
	else if (bf == -1) {
		//subLR左边的节点会给到subL的右边
		//所以subL平衡,parent右边高
		subL->_bf = 0;
		parent->_bf =1;
		subLR->_bf = 0;

	}
}
//右左双旋
void RotateRL(Node* parent) {

	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	//本质是将subLR的左或者右节点分配给subL或者parent
	int bf = subRL->_bf;

	//对两个节点进行左旋右旋
	RotateL(subR);
	RotateR(parent);

	if (bf == 0) {
		//此时只有三个节点,旋转完成后全部平衡
		subR->_bf = 0;
		parent->_bf = 0;
		subRL->_bf = 0;
	}
	else if (bf == 1) {
		//subLRL右边的节点会给到parent的左边
		//所以subR平衡,parent左边高
		subR->_bf = 0;
		parent->_bf = -1;
		subRL->_bf = 0;

	}
	else if (bf == -1) {
		//subLR左边的节点会给到parnet的右边
		//所以parent平衡,subR右边高
		subR->_bf = 1;
		parent->_bf = 0;
		subRL->_bf = 0;

	}
}

7.AVL树的平衡检验

我们只需要对每个节点都检验,它的左右子树高度差是否超过1即可,所以先写一个计算二叉树高度的函数,然后递归整棵树,复用计算左右子树高度的代码,令它们相减即可

cpp 复制代码
	int Height(Node* root) {
		if (root == nullptr) {
			return 0;
		}
		return (Height(root->_left) > Height(root->_right) ? Height(root->_left) : Height(root->_right)) + 1;
	}
	bool _IsBanlance(Node* root) {

		if (root == nullptr) {
			return true;
		}

		int LHeight = Height(root->_left);
		int RHeight = Height(root->_right);

		int bf = RHeight - LHeight;
		if (abs(bf) > 2 || bf != root->_bf) {

			cout << "平衡因子异常" << endl;
			return false;
		}

		return _IsBanlance(root->_left) && _IsBanlance(root->_right);

	}
相关推荐
咸鱼翻身小阿橙1 小时前
Qt Quick QML 登录界面代码学习报告
开发语言·qt·学习
小夏子_riotous1 小时前
Kubernetes学习路径——3. Kubernetes 1.25 高可用集群部署实战:从 Docker 到 Calico 全链路详解
linux·运维·学习·docker·容器·kubernetes·centos
今天也是元气满满的一天呢2 小时前
20260512-SQL学习大览
数据库·sql·学习
小新同学^O^2 小时前
简单学习 --> Cookie 和Session
学习
咸甜适中2 小时前
rust语言学习笔记Trait之Default(默认值)
笔记·学习·rust
沪漂阿龙2 小时前
AI大模型面试题:模型求解和优化全解析——梯度下降、BGD、SGD、MBGD、学习率、Batch Size、损失函数、优化器一文讲透
人工智能·学习·机器学习
AOwhisky2 小时前
Docker 学习笔记:网络篇
linux·运维·网络·笔记·学习·docker·容器
24白菜头2 小时前
MySQL学习笔记
数据库·笔记·学习·mysql
小陈phd3 小时前
多模态大模型学习笔记(三十九)——生成式与Transformer式OCR:从“像素抄录“到“文档智能“的完整演进
笔记·学习·transformer