AVL模拟实现以及四种旋转方式图解

AVL树概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。

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

性质

①左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
②如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,搜索时间复杂度O(log2^n)

模拟实现

首先,我们对AVL树中的节点进行一个封装:

template<class K,class V>
struct AVLTreeNode
{
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	pair<K, V> _kv;
	int _bf;  // balance factor

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

AVL的重点操作放在插入节点上,删除节点暂时不涉及。

Insert

插入一个节点第一步找到插入位置,同时比较插入位置cur与parent的大小,来确定是插在parent的左子树上还是右子树上。

template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
bool Insert(const pair<K, V>& kv)
{
	// 根为空,插入的节点就为根节点
	if (_root == nullptr)
	{
		_root = new Node(kv);
		return true;
	}
	
	//根不为空,找到插入位置,进行插入
	Node* cur = _root;
	Node* parent = nullptr;
	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);  // cur的位置就是新插入的位置
	if (parent->_kv.first > kv.first)  // 使得新插入节点和上面的源节点产生连接,先确定插入位置是左孩子还是右孩子
	{
		parent->_left = cur;
	}
	else if (parent->_kv.first < kv.first)
	{
		parent->_right = cur;
	}
	cur->_parent = parent;

private:
Node* _root = nullptr;
}

接着,因为AVL树是通过平衡因子来控制整棵树始终保持平衡的,所以我们在插入之后要更新平衡因子
默认右子树为正,插入在父亲的左子树平衡因子-1,插入在父亲的右子树平衡因子+1

if (cur == parent->_left)  parent->_bf--;
else if (cur == parent->_right) parent->_bf++;

插入之后,我们还得判断这颗树是不是平衡的状态:
如果插入变为0,说明原先为1或-1,新插入节点不影响树的整体高度

如果插入后变为1或-1,说明原先为0,此时树的高度受到了影响,但没失衡,此时继续向上更新观察平衡因子

如果插入后变为2或-2,说明原先为1或-1,此时已经完全失衡了,需要进行旋转
那么具体怎么旋转可以接着往下面看

if (parent->_bf == 0)   // 原先为1或-1,插入变为0。说明新插入节点不影响树的整体高度
{
	break;  
}
else if (parent->_bf == 1 || parent->_bf == -1)   // 原先为0,插入后变为1或-1。此时树的高度受到了影响
{
	// 继续向上更新
	cur = parent;
	parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)  // 原先为1或-1,此时已经完全失衡了,需要进行旋转
{
	// 此时分为4种情况,要分开讨论
	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);
	}
	
	break;  // 旋转完之后就一定是平衡的状态了,直接break循环
}

新节点插入较高左子树的左侧—左左:右单旋

如果(parent->_bf 等于 -2 && cur->_bf 等于 -1),此时需要进行右旋
抽象图:
在这里插入图片描述

在这里插入图片描述

很明显,左边严重偏高。呈现抽象图的形状,我们就可以采用右旋来调节了。
我们需要将parent,subL,subLR记录下来,然后将subL作为这棵树新的根节点,把subL的右节点subLR给到parent,让parent的左子树不再是subL而是subLR。
在链接的时候,要注意前后的衔接,同时需要注意两点,①subLR可能为空节点 ②这里所谓的parent可能还有根节点,所以还要对根节点的情况进行判断
这些做完之后就要改变相应的平衡因子,单旋的平衡因子是固定的,通过画图就可以得知。
旋转之后,AVL树就达到了平衡,就不需要再去调节了。

具体代码:

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

	parent->_left = subLR;
	if (subLR) subLR->_parent = parent;  // 这里判空是因为subLR可能为空节点
	subL->_right = parent;
	parent->_parent = subL;

	if (parent == _root)  // 更新根节点
	{
		_root = subL;
		subL->_parent = nullptr;  // subL的parent要更新成nullptr
	}
	else
	{
		if (ppNode->_left == parent)
		{
			ppNode->_left = subL;
		}
		else if (ppNode->_right == parent)
		{
			ppNode->_right = subL;
		}
		subL->_parent = ppNode;
	}

	parent->_bf = subL->_bf = 0;
}

新节点插入较高右子树的右侧—右右:左单旋

如果(parent->_bf 等于 2 && cur->_bf 等于 1),此时需要进行右旋
抽象图:在这里插入图片描述
在这里插入图片描述
很明显,右边严重偏高。呈现抽象图的形状,我们就可以采用左旋来调节了。
我们需要将parent,subR,subRL记录下来,然后将subR作为这棵树新的根节点,把subR的左节点subRL给到parent,让parent的左子树不再是subR而是subRL。
在链接的时候,要注意前后的衔接,同时需要注意两点,①subRL可能为空节点 ②这里所谓的parent可能还有根节点,所以还要对根节点的情况进行判断
这些做完之后就要改变相应的平衡因子,单旋的平衡因子是固定的,通过画图就可以得知。
旋转之后,AVL树就达到了平衡,就不需要再去调节了。

具体代码:

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

	parent->_right = subRL;
	if (subRL) subRL->_parent = parent;  // 这里判空是因为subRL可能为空节点
	subR->_left = parent;
	parent->_parent = subR;

	if (parent == _root)  // 更新根节点
	{
		_root = subR;
		subR->_parent = nullptr;  // subR的parent要更新成nullptr
	}
	else
	{
		if (ppNode->_left == parent)
		{
			ppNode->_left = subR;
		}
		else if (ppNode->_right == parent)
		{
			ppNode->_right = subR;
		}
		subR->_parent = ppNode;
	}

	parent->_bf = subR->_bf = 0;
}

新节点插入较高左子树的右侧—左右:先左单旋再右单旋

如果(parent->_bf 等于 -2 && cur->_bf 等于 1),此时需要进行双旋,先左单旋再右单旋
抽象图:在这里插入图片描述
在这里插入图片描述
呈现抽象图的形状,我们就可以采用先左单旋再右单旋来调节了。
上图写了,双旋的平衡因子结果会受到subLR平衡因子的影响。 subLR的左右节点会分别称为subL的右节点和parent的左节点,影响两者的平衡因子,所以在后续的代码需要进行分情况讨论。
我们需要将parent,subL,subLR记录下来,因为此时的形状用单旋已经达不到效果了,此处我们需要运用到双旋。 我们先对subL进行左旋,接着对parent进行右旋就可以达到效果。
在链接的时候,要注意前后的衔接,同时需要注意两点,①此处,subLR一定不是空节点了,这里与上面的情况不同了,这里是一个弯下来的状态,所以这个subLR这个节点是一定包含在里面的,所以不要判它为是否为空 ②同样的,这里所谓的parent可能还有根节点,所以还要对根节点的情况进行判断
这些做完之后就要改变相应的平衡因子,单旋的平衡因子是固定的,通过画图就可以得知。
旋转之后,AVL树就达到了平衡,就不需要再去调节了。
具体代码:

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

	int bf = subLR->_bf;  // 需要提前记录,后面旋转之后会变成0
	RotateL(subL);
	RotateR(parent);

	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 if (bf == 0)   // 这种情况属于subLR节点左右节点都有   不存在都没有的情况,因为那样就属于平衡的情况了
	{
		parent->_bf = 0;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

新节点插入较高右子树的左侧—右左:先右单旋再左单旋

如果(parent->_bf 等于 2 && cur->_bf 等于 -1),此时需要进行双旋,先右单旋再左单旋
抽象图:
在这里插入图片描述
在这里插入图片描述
呈现抽象图的形状,我们就可以采用先右单旋再左单旋来调节了。
上图写了,双旋的平衡因子结果会受到subRL平衡因子的影响。 subRL的左右节点会分别称为parent的右节点和subR的左节点,影响两者的平衡因子,所以在后续的代码需要进行分情况讨论。
我们需要将parent,subR,subRL记录下来,因为此时的形状用单旋已经达不到效果了,此处我们需要运用到双旋。 我们先对subR进行右旋,接着对parent进行左旋就可以达到效果。
在链接的时候,要注意前后的衔接,同时需要注意两点,①此处,subRL一定不是空节点了,这里与上面的情况不同了,这里是一个弯下来的状态,所以这个subRL这个节点是一定包含在里面的,所以不要判它为是否为空 ②同样的,这里所谓的parent可能还有根节点,所以还要对根节点的情况进行判断
这些做完之后就要改变相应的平衡因子,单旋的平衡因子是固定的,通过画图就可以得知。
旋转之后,AVL树就达到了平衡,就不需要再去调节了。
具体代码:

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

	int bf = subRL->_bf;  // 需要提前记录,后面旋转之后会变成0
	RotateR(subR);
	RotateL(parent);

	if (bf == 1)
	{
		parent->_bf = -1;
		subR->_bf = 0;
		subRL->_bf = 0;
	}
	else if (bf == -1)
	{
		parent->_bf = 0;
		subR->_bf = 1;
		subRL->_bf = 0;
	}
	else if (bf == 0)   
	{
		parent->_bf = 0;
		subR->_bf = 0;
		subRL->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

最后就是一些测试用到的一些代码

测试运用的代码

	int _Size(Node* root)
	{
		return root == nullptr ? 0 : _Size(root->_left) + _Size(root->_right) + 1;
	}

	int _Height(Node* root)
	{
		if (root == nullptr)
			return 0;
		return max(_Height(root->_left) , _Height(root->_right)) + 1;
	}

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

		_InOrder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_InOrder(root->_right);
	}

	bool _IsBalance(Node* root)
	{
		if (root == nullptr)
		{
			return true;
		}

		int leftHeight = _Height(root->_left);
		int rightHeight = _Height(root->_right);

		if (abs(leftHeight - rightHeight) >= 2)
		{
			return false;
		}

		// 对平衡因子进行检查
		if (rightHeight - leftHeight != root->_bf)  //前面判断了高度,但这里还是建议对平衡因子进行检查,双保险
		{
			return false;
		}

		return _IsBalance(root->_left)
			&& _IsBalance(root->_right);
	}
};
  • 9
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值