数据结构 - AVLTree

目录

一、概念

二、实现

  2.1 节点

2.2 树

2.3 插入

2.4 旋转

2.4.1 左旋

2.4.2 右旋

2.4.3 右左双旋

2.4.4 左右双旋

2.5 验证


一、概念

        AVL树来源于二叉搜索树。当二叉搜索树是以下单支的情况下,搜索效率退化为O(n)。为了解决这个问题,我们通过一些手段将二叉搜索树左右平衡(每一个节点的左右子树高度差不大于1),这样平衡的二叉搜索树就是AVL树。

        为了更好地实现AVL树,我们引入平衡因子(balance factor)的概念,简写为bf。每个节点都有平衡因子,并且值为该节点右子树高度减左子树高度。AVL树的平衡因子绝对值不能大于1。


二、实现

  2.1 节点

        除了正常树中的记录左右节点的地址、数据外,还需要记录父亲节点的地址和平衡因子的记录。根节点的父亲节点为空。

template<class T>
struct AVLTreeNode
{
	AVLTreeNode(const T& data = T())
		: _pLeft(nullptr)
		, _pRight(nullptr)
		, _pParent(nullptr)
		, _data(data)
		, _bf(0)
	{}

	AVLTreeNode<T>* _pLeft;
	AVLTreeNode<T>* _pRight;
	AVLTreeNode<T>* _pParent;
	T _data;
	int _bf;   // 节点的平衡因子
};

2.2 树

        成员函数中主要难点是插入函数。私有成员中的左右旋函数就是对不平衡二叉搜索树平衡的手段。

// AVL: 二叉搜索树 + 平衡因子的限制
template<class T>
class AVLTree
{
	typedef AVLTreeNode<T> Node;
public:
	AVLTree()
		: _pRoot(nullptr)
	{}

	// 在AVL树中插入值为data的节点
	bool Insert(const T& data);

	// AVL树的验证
	bool IsAVLTree()
	{
		return _IsAVLTree(_pRoot);
	}

private:
	// 根据AVL树的概念验证pRoot是否为有效的AVL树
	bool _IsAVLTree(Node* pRoot);
	size_t _Height(Node* pRoot);
	// 右单旋
	void RotateR(Node* pParent);
	// 左单旋
	void RotateL(Node* pParent);
	// 右左双旋
	void RotateRL(Node* pParent);
	// 左右双旋
	void RotateLR(Node* pParent);

private:
	Node* _pRoot;
};

2.3 插入

        插入的整个过程:1.找到插入节点并直接插入。2.由插入节点倒退回根节点对节点的平衡因子进行更新。3.根据平衡因子的更新情况,决定是否要对子树进行旋转平衡。

        平衡因子更新情况:1.bf = 0,该节点左右子树高度变为相同,以该节点为根节点的子树高度不变,于是不需要再往上更新了。2. bf = 1 || bf = -1,以该节点为根节点的子树高度加一,需要继续往上更新,若一直更新到根节点就停止。3.bf = 2 || bf = -2 ,二叉树已经不平衡了,需要旋转操作改变结构来平衡以该节点为根节点的子树。

    // 在AVL树中插入值为data的节点
	bool Insert(const T& data)
	{
		if (_pRoot == nullptr)
		{
			_pRoot = new Node(data);
		}
		else
		{
			//找到插入节点,并插入
			Node* cur = _pRoot;
			Node* parent = nullptr;
			while (cur)
			{
				if (data < cur->_data)
				{
					parent = cur;
					cur = cur->_pLeft;
				}
				else if (data > cur->_data)
				{
					parent = cur;
					cur = cur->_pRight;
				}
				else
					return false;
			}
			cur = new Node(data);
			if(parent->_data > cur->_data)
				parent->_pLeft = cur;
			else
				parent->_pRight = cur;
			cur->_pParent = parent;
			while (parent)
			{
				//更新平衡因子
				if (parent->_pLeft == cur)
					parent->_bf--;
				else
					parent->_bf++;
				//检测平衡因子
				if (parent->_bf == 0)
					break;
				else if (parent->_bf == 1 || parent->_bf == -1)
				{
					//继续向上更新
					cur = parent;
					parent = parent->_pParent;
				}
				else if (parent->_bf < -1 || parent->_bf > 1)
				{
					//出现平衡因子绝对值大于1,需要旋转平衡
					if (parent->_bf == -2 && cur->_bf == -1)
						RotateR(parent);
					if (parent->_bf == 2 && cur->_bf == 1)
						RotateL(parent);
					if (parent->_bf == -2 && cur->_bf == 1)
						RotateLR(parent);
					if (parent->_bf == 2 && cur->_bf == -1)
						RotateRL(parent);
					break;
				}
				else // 插入之前平衡因子就有问题
					assert(false);
			}
		}
	}

2.4 旋转

2.4.1 左旋

        需要左旋的情况如下,由于2节点右子树高度增加,导致1节点bf=2>1,二叉树失衡。于是我们对值为1的节点进行左旋,具体做法就是,将1节点的右子树指针指向2节点的左节点,再让2节点的左子树指针指向1节点。旋转完成后1、2 节点的bf都为0。

        以下是链接过程的代码实现,需要注意的是,每两个节点之间是有两条链的,在对父亲节点的链接前要判断一下,自身节点是不为空的。

// 左单旋
	void RotateL(Node* pParent)
	{
		Node* childR = pParent->_pRight;
		Node* childRL = childR->_pLeft;
		Node* ppParent = pParent->_pParent;
		pParent->_pRight = childRL;
		if (childRL)
			childRL->_pParent = pParent;
		childR->_pLeft = pParent;
		pParent->_pParent = childR;
		if (_pRoot == pParent)
		{
			_pRoot = childR;
			childR->_pParent = nullptr;
		}
		else
		{
			if (ppParent->_pLeft == pParent)
				ppParent->_pLeft = childR;
			else
				ppParent->_pRight = childR;
			childR->_pParent = ppParent;
		}
		childR->_bf = pParent->_bf = 0;
	}

2.4.2 右旋

        与左旋是相反的情况。2节点bf = -2 < -1,将2节点右旋。2节点的左指针指向1节点的右孩子,1节点的右指针指向2节点。并完成相应父节点的链接。

    // 右单旋
	void RotateR(Node* pParent)
	{
		Node* childL = pParent->_pLeft;
		Node* childLR = childL->_pRight;
		Node* ppParent = pParent->_pParent;
		pParent->_pLeft = childLR;
		if (childLR)
			childLR->_pParent = pParent;
		childL->_pRight = pParent;
		pParent->_pParent = childL;
		if (pParent == _pRoot)
		{
			_pRoot = childL;
			_pRoot->_pParent = nullptr;
		}
		else
		{
			if (ppParent->_pLeft == pParent)
				ppParent->_pLeft = childL;
			else
				ppParent->_pRight = childL;
			childL->_pParent = ppParent;
		}
		childL->_bf = pParent->_bf = 0;
	}

2.4.3 右左双旋

        由于2节点子树的高度增加导致1节点的右子树高度增加,1节点bf>1失衡。这时先对3节点右旋,形成下图右。下图右不就是以上需要左旋的情况吗,于是我们在对1节点进行左旋即可得到平衡二叉树。

         对1节点进行左旋。

         代码实现只需要对左右旋函数进行复用即可,需要注意的是平衡因子的更新。对最下面的节点(上图中2节点)的平衡因子进行判断。如果其值为-1则插入在左边,那么最终右边的3节点平衡因子不为0,应更新为1。同理可知值为1和值为0下的平衡因子更新情况。

	// 右左双旋
	void RotateRL(Node* pParent)
	{
		Node* subR = pParent->_pRight;
		Node* subRL = subR->_pLeft;
		int bf = subRL->_bf;
		RotateR(pParent->_pRight);
		RotateL(pParent);
		if (bf == 1)
		{
			pParent->_bf = -1;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == -1)
		{
			pParent->_bf = 0;
			subR->_bf = 1;
			subRL->_bf = 0;
		}
		else if (bf == 0)
		{
			pParent->_bf = 0;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else
			assert(false);
	}

2.4.4 左右双旋

        逻辑完全与右左双旋相同,把左右调换一下即可。

	// 左右双旋
	void RotateLR(Node* pParent)
	{
		Node* subL = pParent->_pLeft;
		Node* subLR = subL->_pRight;
		int bf = subLR->_bf;
		RotateL(pParent->_pLeft);
		RotateR(pParent);
		if (bf == 1)
		{
			pParent->_bf = 0;
			subL->_bf = -1;
			subLR->_bf = 0;
		}
		else if (bf == -1)
		{
			pParent->_bf = 1;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else if (bf == 0)
		{
			pParent->_bf = 0;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else
			assert(false);
	}

2.5 验证

        验证一颗AVLTree要从根节点开始,不断往下去检查左右子树的高度差是否小于2。

    bool _IsAVLTree(Node* pRoot)
	{
		if (pRoot == nullptr)
			return true;

		int leftHeight = _Height(pRoot->_pLeft);
		int rightHeight = _Height(pRoot->_pRight);
		return abs(rightHeight - leftHeight) < 2
			&& _IsAVLTree(pRoot->_pLeft)&& _IsAVLTree(pRoot->_pRight);
	}
	size_t _Height(Node* pRoot)
	{
		if (root == nullptr)
			return 0;
		int leftHeight = _Height(pRoot->_pLeft);
		int rightHeight = _Height(pRoot->_pRight);
		return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
	}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值