【C++】AVL树

目录


前言

本篇文章我们要讲解的是AVL树,它是为了解决上篇文章我们讲解的二叉搜索树的缺陷,当元素接近有序时二叉树退化成单支树,我们查找的效率十分低下为 O ( N ) O(N) O(N)。那么AVL树是怎么解决这个问题的呢,下面就让我们一起来进行学习吧。

一、AVL树的概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树 ,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年

发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

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

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

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

Q:那么这种方案为什么就能解决问题呢?

如果一棵二叉搜索树是高度平衡的,那么当我们查询某个元素时,就算这棵树非常接近有序时,我们的高度还是保持在 O ( l o g 2 n ) O(log_2n) O(log2n),因此就不存在会一直向下寻找N次的情况!!

二、AVL树的模拟实现

对于AVL树的模拟实现,我们采用的是通过调节平衡因子来控制树的高度,当俩树的高度不符合平衡因子的条件时就需要进行一系列操作来降低树的高度,以此来完成平衡整棵树!!此外由于根节点的高度会影响祖先的高度,这里我们额外使用一个节点来保存祖先节点,方便后续对祖先的高度进行调整!!并且之后每次进行插入结点的操作都要维护_parent节点!!

2.1 AVL树的节点定义

c 复制代码
template <class K, class V>
struct AVLTreeNode
{
	AVLTreeNode<K, V>* _parent;	// 父节点
	AVLTreeNode<K, V>* _left;	// 左孩子
	AVLTreeNode<K, V>* _right;	// 右孩子

	pair<K, V> _kv;	// 键值对
	int _bf;	// balance factor(平衡因子)

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

2.2 AVL树的插入操作

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

  1. 按照二叉搜索树的方式插入新节点
  2. 调整节点的平衡因子

如何来调整结点的平衡因子呢?这里我们先从整体上进行分析,之后再对具体情况进行逐个分析:

好了,通过上述对调整平衡因子的大体分析我们的整体框架如下:

c 复制代码
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;
	else
		parent->_left = cur;

	cur->_parent = parent; // 指向父节点

	// 控制平衡并更新平衡因子
	while (parent)
	{
		// 新插入节点在父节点的右边,则父节点平衡因子自加一
		if (cur == parent->_left)
			--parent->_bf;
		else
			++parent->_bf;

		if (parent->_bf == 0)	// 不需要继续往上更新
		{
			break;
		}
		else if (abs(parent->_bf) == 1)	// 继续往上更新
		{
			parent = parent->_parent;
			cur = cur->_parent;
		}
		else if (abs(parent->_bf) == 2)
		{
			// 说明parent所在的子树已经不平衡了,需要旋转处理
			
			break; // 旋转一次即可保持树的高度平衡
		}
		else	// 理论上不会走到这里
		{
			assert(false);
		}
	}

	return true;
}

2.3 AVL树的旋转

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

  • 新节点插入较高右子树的右侧---右右:左单旋
c 复制代码
void RotateL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	parent->_right = subRL;
	if (subRL) // 有可能为空
		subRL->_parent = parent;
	
	// 记录父节点的祖先
	Node* ppnode = parent->_parent;

	subR->_left = parent;
	parent->_parent = subR;
    // 如果parent的祖先为nullptr,说明此前它为根节点  subR更新为_root;
	if (ppnode == nullptr)
	{
		_root = subR;
		_root->_parent = nullptr;
	}
	else
	{
		if (ppnode->_left == parent)
		{
			ppnode->_left = subR;
		}
		else
		{
			ppnode->_right = subR;
		}
		subR->_parent = ppnode;
	}
    // 更新平衡因子
	subR->bf = parent->bf = 0;
}
  • 新节点插入较高左子树的左侧---左左:右单旋
c 复制代码
void RotateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	parent->_left = subLR;
	if (subLR)
		subLR->_parent = parent;

	Node* ppnode = parent->_parent;
	
	subL->_right = parent;
	parent->_parent = subL;

	if (ppnode == nullptr)
	{
		_root = subL;
		_root->_parent = nullptr;
	}
	else
	{
		if (ppnode->_left == parent)
		{
			ppnode->_left = subL;
		}
		else
		{
			ppnode->_right = subL;
		}
		subL->_parent = ppnode;
	}
	subL->bf = parent->bf = 0;
}
  • 新节点插入较高左子树的右侧---左右:先左单旋再右单旋

为什么要使用双旋,单旋不能解决吗?

下面我们就来分析一下左右双旋该如何处理:

c 复制代码
void RotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	int bf = subLR->bf;

	RotateL(parent->_left); // 左旋
	RotateR(parent); // 右旋

	if (bf == 1)
	{
		parent->bf = 0;
		subLR->bf = 0;
		subL->bf = -1;
	}
	else if (bf == -1)
	{
		parent->bf = 1;
		subLR->bf = 0;
		subL->bf = 0;
	}
	else if (bf == 0)
	{
		parent->bf = 0;
		subLR->bf = 0;
		subL->bf = 0;
	}
	else
	{
		assert(false);
	}
}
  • 新节点插入较高右子树的左侧---右左:先右单旋再左单旋

好了,关于四种情况的旋转已经是通过画图给大家分析的清清楚楚的了,下面我们就来看看它们的代码:

c 复制代码
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;
		subRL->bf = 0;
		subR->bf = 0;
	}
	else if (bf == -1)
	{
		parent->bf = 0;
		subRL->bf = 0;
		subR->bf = 1;
	}
	else if (bf == 0)
	{
		parent->bf = 0;
		subRL->bf = 0;
		subR->bf = 0;
	}
	else
	{
		assert(false);
	}
}

另外我们还要分析出出现这四种情况对应的条件:

三、AVL树的总结

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

1.pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR.

当pSubR的平衡因子为1时,执行左单旋.

当pSubR的平衡因子为-1时,执行右左双旋.

2.pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL.

当pSubL的平衡因子为-1是,执行右单旋.

当pSubL的平衡因子为1时,执行左右双旋.

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

因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置,所以删除结点的情况实际是更为复杂的,大家感兴趣的自行去查阅资料实现吧!!

四、AVL树的性能

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即O( l o g 2 N log_2 N log2N)。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。所以接下来就用到了更为极致的方式去解决问题------红黑树!!又是一位巨佬的杰作。

相关推荐
CYBEREXP200832 分钟前
MacOS M3源代码编译Qt6.8.1
c++·qt·macos
yuanbenshidiaos1 小时前
c++------------------函数
开发语言·c++
yuanbenshidiaos1 小时前
C++----------函数的调用机制
java·c++·算法
tianmu_sama1 小时前
[Effective C++]条款38-39 复合和private继承
开发语言·c++
羚羊角uou2 小时前
【C++】优先级队列以及仿函数
开发语言·c++
姚先生972 小时前
LeetCode 54. 螺旋矩阵 (C++实现)
c++·leetcode·矩阵
FeboReigns2 小时前
C++简明教程(文章要求学过一点C语言)(1)
c语言·开发语言·c++
FeboReigns2 小时前
C++简明教程(文章要求学过一点C语言)(2)
c语言·开发语言·c++
264玫瑰资源库2 小时前
从零开始C++棋牌游戏开发之第二篇:初识 C++ 游戏开发的基本架构
开发语言·c++·架构
_小柏_3 小时前
C/C++基础知识复习(43)
c语言·开发语言·c++