C++进阶——AVL树

1.ALV树的概念以及介绍

ALV树是二叉搜索树的改良版本,AVL树的出现优化了二叉搜索树的一些缺陷。二叉搜索树虽可以缩短查找的效率,但如果对于有序数据的查找或接近有序的数据,二叉搜索树的两侧树高将会相差甚大,这会导数二叉搜索树退化为单支树,查找元素相当于在顺序表中搜索元素,搜索的效率将会变得低下。
后来,为了解决二叉搜索树的这一缺陷,出现了两位俄罗斯的数学家:G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1,使得左右子树两侧的高度达到近似相等,从而减少平均搜索长度,提高搜索效率。

2.AVL树的特性

AVL树的前提是一颗二叉搜索树,我们可以认为,AVL树是在二叉搜索树的基础上增加了平衡因子这一变量来控制二叉搜索树的左右树高。AVL树可以是一颗空树,或者是具有以下性质前提的二叉搜索树。

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

 

3.AVL树的基本结构

AVL树的结构是基于二叉搜索树为前提的,在二叉搜索树的结构下,AVL树的存放的数据应该多了一个用于控制树高的平衡因子。此外为了调整左右子树树高同时更新上一节点的平衡因子(其大小设置为右侧树高 - 左侧树高),需要额外增加一个指向上一节点的成员。其余的结构与二叉搜索树是一致的。

对于AVL树,主要讨论的是AVL树数据插入的过程,这个过程比二叉搜索树复杂很多,需要仔细了解。

template <class K>
struct AVLTreeNode
{
	AVLTreeNode<K>* _left;//左节点
	AVLTreeNode<K>* _right;//右节点
    AVLTreeNode<K>* _parent;//上一节点
	K _key;//节点值大小
	int _bf;//平衡因子 = 右侧树高 - 左侧树高

	AVLTreeNode(const K key)
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
        ,_bf(0)
	{}
};

template <class K>
class AVLTree
{
	typedef AVLTreeNode<K> Node;
private: Node* _root = nullptr;
public:

}

3.2AVL树的数据插入

bool Insert(const K& key)
{
	if (_root == nullptr)//根节点为空
	{
		_root = new Node(key);
		return true;
	}
    Node* cur = _root;
	Node* curParent = nullptr;
	while (cur)
	{
		if (cur->_key > key)//根节点大,往左边插入
		{
			curParent = cur;
			cur = cur->_left;

		}
		else if (cur->_key < key)//根节点小,往右边插入
		{
			curParent = cur;
			cur = cur->_right;

		}
		else//插入数据重复,插入失败
		{
			return false;
		}
	}
	cur = new Node(key);
	if (key < curParent->_key)//左插入
	{
		curParent->_left = cur;
	}
	else//右插入
	{
		curParent->_right = cur;
	}
    cur->_parent = curParent;//指向上一节点

    //调整树的平衡因子
	while (parent)
	{
		if (parent->_left == cur)//左子树
		{
			parent->_bf--;
		}
		else//右子树
		{
			parent->_bf++;
		}
		if (parent->_bf == 0)//树平衡
		{
			break;
		}
		else if (parent->_bf == -1 || parent->_bf == 1)//可能出现不平衡,向上检查
		{
			cur = parent;
			parent = cur->_parent;
		}
		else if (parent->_bf == 2 || parent->_bf == -2)//树不平衡,旋转调整
		{
			//旋转后:1.保持依然是搜索二叉树,2.树平衡
			if (parent->_bf == 2)//右子树高
			{
				if (cur->_bf == 1)//右子树插入,左单旋
				{
					RotateL(parent);
				}
				else if (cur->_bf == -1)//左子树插入,右左双旋
				{
					RotateRL(parent);
				}
			}
			else//左子树高
			{
				if (cur->_bf == -1)//左子树插入
				{
					RotateR(parent);
				}
				else if (cur->_bf == 1)//右子树插入,左右双旋
				{
					RotateLR(parent);
				}
			}
		}
	}

	return true;
}
	

对于AVL树的数据插入,是十分麻烦的,在保持二叉搜索树的插入数据的前提下,我们增加了每个节点指向上一节点的前提,这个是很容易的,但是难点在于对于每次插入数据之后树高度的调整,我们需要确保每次插入数据之后,所以节点左右子树的高度差不超过1,对于左右子树树高的调整,我们需要通过平衡因子确定,对于不同情况下的平衡因子需要采取不同的方法控制树高,具体控制方法我们称之为树的旋转。下面会对针不同情况下的插入数据进行分析。

插入节点时后,上一节点的变换影响着是否要进行旋转调整子树高度。对应以下几种情况

  1. 插入节点后,当上一节点的平衡因子变为0时,说明树高未被改变,插入新的节点反而填补了左右两树高不平衡的问题,所以也不需要向上调整平衡因子。
  2. 插入节点后,当上一节点的平衡因子变为-1或1时,说明树高被改变,插入节点前树为平衡状态,需要调整上一节点的平衡因子。当继续找到上一节点的平衡因子为-2或者2时,说明此时需要调整树高。

以下对于开始调整树高位置的节点我们称之为parent,以parent相对位置左右则命名为subL、subR, 对应的subRL、subLR则是对应subR、subL的相对位置。

单侧左旋

这种情况下的应对的是新插入的节点位置为较高右子树的右侧,意思就是插入节点前二叉树的右子树高于左子树,而插入位置为右子树的右侧。即为右高插右,左旋

此时的旋转操作为:将parent的右节点subR取代parent的位置,将subR的左节点链接parent的右节点,充当parent的右子树,同时需要注意调整各个节点的上一指向

此时subR、parent的平衡因子均为0。

void RotateL(Node* parent)
{
	Node* ppNode = parent->_parent;
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	parent->_right = subRL;
	if (subRL)//判断子树是否为空
		subRL->_parent = parent;

	subR->_left = parent;
	parent->_parent = subR;
	if (parent == _root) //parent为根节点
	{
		subR->_parent = nullptr;
		_root = subR;
	}
	else
	{
		subR->_parent = ppNode;
		if (ppNode->_left == parent)// parent为上一节点的左子树
		{
			ppNode->_left = subR;
		}
		else// parent为父节点的右子树
		{
			ppNode->_right = subR;
		}
	}
	parent->_bf = subR->_bf = 0;//树平衡
}

单侧右旋

这种情况下的应对的是新插入的节点位置为较高左子树的左侧,意思就是插入节点前二叉树的左子树高于右子树,而插入位置为左子树的左侧。 左高插左,右旋

此时的旋转操作为:将parent的左节点subL取代parent的位置,将subL的右节点链接parent的左节点,充当parent的左子树,同时需要注意调整各个节点的上一指向 

此时subR、parent的平衡因子均为0。

	void RotateR(Node* parent) //右单旋
	{
		Node* ppNode = parent->_parent;
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		parent->_left = subLR;
		if(subLR) //判断子树是否为空
			subLR->_parent = parent;

		subL->_right = parent;
		parent->_parent = subL;

		if (parent == _root) //parent为根节点
		{
			subL->_parent = nullptr;
			_root = subL;
		}
		else
		{
			subL->_parent = ppNode;
			if (ppNode->_left == parent) //parent为父节点的左子树
			{
				ppNode->_left = subL;
			}
			else //parent为父节点的右子树
			{
				ppNode->_right = subL;
			}
		}
		parent->_bf = subL->_bf = 0;//树平衡
	}

左右双旋(先左旋,再右旋)

 这种情况下的应对的是新插入的节点位置为较高左子树的右侧,意思就是插入节点前二叉树的左子树高于右子树,而插入位置为左子树的右侧。

此时的旋转操作为:先将parent的左节点subL进行左旋转调整(可复用单侧左旋),然后再将节点parent进行右旋转调整(可复用单侧右旋),同时需要注意调整各个节点的上一指向 

此时subLR平衡因子为0,parent与subL的平衡因子取决于插入后subL的平衡因子大小,也就是subL节点插入位置。

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

	RotateL(subL);
	RotateR(parent);

	if(bf == -1)//subLR左树高
	{
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 1;
	}
	else if (bf == 1)//subLR右树高
	{
		subLR->_bf = 0;
		subL->_bf = -1;
		parent->_bf = 0;
	}
	else//subLR为插入节点,其左右子树为空
	{
		parent->_bf = subL->_bf = subLR->_bf = 0;
	}

}

右左双旋(先右旋,再左旋)

 

这种情况下的应对的是新插入的节点位置为较高右子树的左侧,意思就是插入节点前二叉树的右子树高于左子树,而插入位置为右子树的左侧。

此时的旋转操作为:先将parent的左节点subL进行左旋转调整(可复用单侧左旋),然后再将节点parent进行右旋转调整(可复用单侧右旋),同时需要注意调整各个节点的上一指向 

此时subLR平衡因子为0,parent与subL的平衡因子取决于插入后subL的平衡因子大小,也就是subL节点插入位置。

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

	RotateR(subR);
	RotateL(parent);

	if (bf == -1)//subRL左树高
	{
		subRL->_bf = 0;
		subR->_bf = 1;
		parent->_bf = 0;
	}
	else if (bf == 1)//subRL右树高
	{
		subRL->_bf = 0;
		subR->_bf = 0;
		parent->_bf = -1;
	}
	else//subRL为插入节点,左右子树为空
	{
		parent->_bf = subR->_bf = subRL->_bf = 0;
	}
}

4.AVL树的优缺点 

相比于二叉搜索树,AVL树解决了二叉搜索树对于有序数据或接近有序的数据查找效率低的问题,对于查找有序或接近有序数据的效率变得很高,其查找的时间复杂度是log(N)。

但同时我们需要了解的是AVL树为了严格维持二叉搜索树各个左右子树的平衡,需要通过旋转的方式来调整左右子树的高度,如果要对AVL树做一些结构修改的操作,其效率性能会非常低,每次对数据的增删都需要进行旋转,而旋转这一调整过程实际上是十分消耗性能的。

对于AVL树的使用,其更多的是用于存储查找数据,如果是需要经常增删操作,AVL树实际上是并不适合的选择。

上一篇:网络带宽对于服务器的影响