初识C++ · AVL树(1)

目录

前言:

1 AVL树的创建

2 部分成员函数

2.1 查找

2.2 中序遍历

2.3 插入

2.4 左旋转

2.5右旋转


前言:

上文,上上文提到了map set,二叉搜索树,其实都是为了近两文做铺垫的,虽然map的底层是红黑树,但是也为AVL树的学习打下了基础,那么什么是AVL树呢?由谁发明的呢?
两位俄罗斯的数学家G.M.Adelson-Velskii 和E.M.Landis发明的,名字就是发明家的首字母咯,发明是用来干什么的呢?发明出来是为了解决当树的结构趋近于单链表时候,效率接近O(N)的问题,只要能有效降低高度,那么就能提高速度,所以AVL树,诞生了。

创建AVL树有很多种方式,我们本次介绍的是使用的平衡因子的概念,也就是每个节点都有个平衡因子,绝对值不能超过1,也就是取值范围是0 1 -1,如果超过2,也就代表树失衡了,需要进行旋转调整。

这里旋转右子树高度减左子树高度的值作为平衡因子的大小,当然,AVL树的满足条件就是,左右子树都是AVL树,并且每个节点的平衡因子都小于2。


1 AVL树的创建

AVL树的创建和二叉平衡搜索树是一样的,无非是每个节点的区别而已,前言,上文提及,节点涉及到的内容有,平衡因子,左右指针,key或者是key-value,这里我们使用key-value模型实现。

但是!当我们旋转的时候,涉及到的节点势必有某个节点的父节点,所以还有一个成员变量,指向父节点的指针,所以AVL树实际上是三叉链的结构:

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

	int _bf;//平衡因子

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

这就是节点的创建。

那么AVL树的大体如下:

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


private:
	Node* _root = nullptr;
};

2 部分成员函数

这里要实现的成员函数有左旋,右旋,以及查找,插入(实现一半),以及中序遍历。

但是部分函数其实变化不打,改的都是细枝末节的东西,这里直接给代码即可:

2.1 查找

	Node* Find(const T& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first < key)
			{
				cur = cur->_right;
			}
			else if (cur->_kv.first > key)
			{
				cur = cur->_left;
			}
			else
			{
				return cur;
			}
		}
		return nullptr;
	}

2.2 中序遍历

	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);

	}

2.3 插入

插入和二叉搜索树都是一样的,但是呢,插入完成之后,平衡因子怎么改变呢?

我们现在不妨来分析一下,平衡因子在插入后会有哪些改变?

当我们在8的左边插入数据后,8的平衡因子变为了0,此时,父节点的平衡因子就不用更新了,因为作为7的右子树来说,8这个子树的高度没有变。

当我们在6的任意部分插入数据,6的平衡因子变为了1或者是-1,此时,需要往上更新数据,7的平衡因子被更新为了0,那么不用往上了,所以此时我们就可以发现一个规律,基于原有的AVL树的基础上,插入数据之后平衡因子变为0的,都不用继续遍历了,因为平衡了,但是要注意是 基于原来的树就是AVL树。

当我们在9的右边插入数据,此时9的平衡因子变为了1,往上更新,8的平衡因子变为了2,那么就需要旋转了,此时是完全的右子树高,所以需要左旋转。

当我们在9的右边插入数据,此时9的平衡因子变为了1,往上更新,8的平衡因子变为了2,那么就需要旋转了,此时是完全的右子树高,所以需要左旋转。

当我们在0的左边边插入数据,此时0的平衡因子变为了-1,往上更新,1的平衡因子变为了-2,那么就需要旋转了,此时是完全的左子树高,所以需要右旋转。

所以插入这里要干的事是更新平衡因子,一直往上更新,直到出现0或者是更新到根节点位置,更新到根节点也就说明了父节点为空,就可以结束了,为0也可以直接结束:

bool Insert(const pair<T,V>& kv)
{
	if (_root == nullptr)
	{
		_root = new Node(kv);
		return true;
	}

	Node* root = _root;
	Node* parent = nullptr;
	//判断部分
	while (root)
	{
		if (kv.first > root->_kv.first)
		{
			parent = root;
			root = root->_right;
		}
		else if (kv.first < root->_kv.first)
		{
			parent = root;
			root = root->_left;
		}
		else
		{
			return false;
		}
	}
	Node* cur = new Node(kv);
	//连接部分 开始判断大小关系
	if (parent->_kv.first > kv.first)
	{
		parent->_left = cur;
	}
	else
	{
		parent->_right = cur;
	}
    cur->_parent = parent;

	//更新平衡因子
	cur->_parent = parent;
	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 = 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;
}

为了保险期间,出现了超过2的情况就要报错了。

现在的问题就是,面对完全的右子树高或者是完全的左子树高我们应该怎么操作?

2.4 左旋转

此时,这棵AVL树就不平衡了,那么我们可以从一个有趣的角度来看,7是当家的,8的二当家,6是三当家,当家的不管事,二当家一不小心做大做强了,那么当家的就得易主了是吧?所以7要下位了,此时8的左子树就应该指向7,那么7的右子树就应该指向8的左子树,空也没有关系。那么旋转完成了吗?没有,现在不够形象,换个结构,我们这样看:

旋转的核心就是这两条线的指向,二号指向7,一号指向7.5,这个过程大家脑部一下,就十分形象了。

但是不要忘了,AVL树实际上是三叉链结构,所以还有父节点需要更新,二号的节点如果不为空,父节点应该指向的是7,7如果不是根节点,那么我们还要用7的父节点作为连接的下一步,判断相对位置,让7的父节点连8,如果是根节点,那么根节点易位就可以了。此时连接的差不多了,但是涉及到的8的父节点还要连接,可能是空,也可能是7的父节点,这里代码给上,结合文字的说明,代码写的也是有区域性的,结合相对来说好理解许多:

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

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

		if (subRL)
		{
			subRL->_parent = parent;
		}

		if (parent == _root)
		{
			_root = subR;
			_root->_parent = nullptr;
		}
		else
		{
			if (parent == pparent->_left)
			{
				pparent->_left = subR;
			}
			else
			{
				pparent->_right = subR;
			}
			subR->_parent = pparent;
		}
		parent->_bf = subR->_bf = 0;
	}

2.5右旋转

右旋转的是同理的,就直接给代码了:

	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		subL->_right = parent;
		parent->_left = subLR;
		Node* pparent = parent->_parent;
		parent->_parent = subL;
		
		if (subLR)
			subLR->_parent = parent;

		if (parent == _root)
		{
			_root = subL;
			_root->_parent = nullptr;
		}
		else
		{
			if (parent == pparent->_left)
			{
				pparent->_left = subL;
			}
			else
			{
				pparent->_right = subL;
			}
			subL->_parent = pparent;
		}
		//更新平衡因子
		parent->_bf = subL->_bf = 0;
	}

以上两种情况是完全的左右子树高,所以只需要一个左旋或者是右旋,下篇是,复合旋转!

至于为什么平衡因子旋转之后一定为0,请见下文~


感谢阅读!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值