【C++之容器篇】AVL树的底层原理和使用

前言

AVL树其实是在搜索树的基础上加上一些限制因素,从而使搜索树的结构保持相对平衡,通过前面我们对二叉搜索树的学习,我们知道,如果我们向一棵二叉搜索树中插入一些有序的数据,那么整棵树就会偏向于一边,从而使整棵树失去平衡,那么在查找的时候就会效率低下,退化为顺序表的效率了,今天我们学习的AVL树本质上就是为了解决二叉搜索树中失去平衡的问题,为了控制二叉搜索树中的子树的左右子树的高度差保持相对平衡,我们采取在树中的每一个结点中增加一个变量来记录每一个结点的平衡因子,我们可以定义平衡因子为该子树的右子树的高度-左子树的高度。

一、AVL树

AVL树是一棵二叉搜索树,并且除了具备二叉搜索树的性质,还具备以下的性质:

  1. 树的左右子树的高度差不超过1,可以通过平衡因子进行控制
  2. 树中的每一棵子树都是AVL树

二、AVL树的底层实现

1. 结点类型的定义

// AVL树的结点
template <class K,class V>
struct AVLTreeNode
{
	// 构造函数
	AVLTreeNode(const pair<K,V>& kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}

	// 成员变量
	pair<K, V> _kv;
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;

	// 平衡因子:左右子树的高度差(右子树高度-左子树高度)
	int _bf;
};

AVL树中的结点需要包含三个指针,即指向左子树的指针,指向右子树的指针,指向父亲结点的指针,还需要一个存储数据的pair结构,还需要有一个平衡因子,其中的构造函数是对一个新生成的结点中的数据进行初始化,新生成的结点中就是通过给定的kv来初始化_kv,然后左子树和右子树和父亲结点均为空,需要确定后再由外面手动更新,其中的平衡因子是由该结点的左右子树的情况来决定的,显然一个新的结点中不存在左右子树,所以平衡因子显然为0。

2. AVL树的定义

template <class K,class V>
class AVLTree
{
	// 使用结点
	typedef AVLTreeNode<K, V> Node;
public:
	// 成员函数

private:
	// 成员变量
	Node* _root = nullptr;
};

和前面学习的树型结构的实现一样,刚开始默认为空树,所以_root给了一个缺省值,在AVL树的类中需要使用到树的结点,所以为了方便表示,可以将树的结点进行重定义。

3. 查找函数

查找函数的实现比较简单,和二叉搜索树中的查找函数的实现几乎一致,只是需要知道,这里的数据是pair类型,pair类型中有first和second,其中first是key,second是value,外面需要通过key去进行查找,也就是需要通过kv.first去进行比较。

	// 查找函数
	bool Find(const pair<K, V>& kv)
	{
		Node* cur = _root;
		while (cur)
		{
			if (kv.first < cur->_kv.first)
			{
				// 左子树找
				cur = cur->_left;
			}
			else if(kv.first>cur->_kv.first)
			{
				// 右子树找
				cur = cur->_right;
			}
			else
			{
				// 找到了
				return true;
			}
		}
		return false;
	}

4. 插入函数(重难点)

AVL树的插入函数的底层其实首先是按照搜索树的规则进行插入,然后再通过平衡因子的调整使整棵树的高度保持相对平衡。

  • 插入的过程
  1. 按照搜索树的规则进行插入
  2. 更新平衡因子,最坏的情况需要沿着路径一直更新到根:如果孩子是插入在左边,则父亲的平衡因子–,如果孩子是插入在父亲的右边,则平衡因子++,当父亲的平衡因子更新之后,需要根据父亲更新后的平衡因子确定作何调整,此时需要进行分类讨论
  • 更新后父亲的平衡因子为0:说明更新前父亲的平衡因子是1或者-1,当更新前父亲的平衡因子是1使,说明这棵子树的右子树比较高,孩子插入在左子树上,所以父亲的高度没有发生变化,不需要继续更新,当更新前父亲的平衡因子是-1时,说明父亲的左子树比较高,此时孩子插入在父亲的右子树上,父亲的高度保持不变,所以不需要更新。
  • 更新后父亲的平衡因子是1或者-1:说明更新前父亲的平衡因子是0,如果更新后父亲的平衡因子变成1,说明孩子插入在父亲的右子树上,导致此时父亲的右子树变高,所以需要继续往上更新。如果更新后父亲的平衡因子变成-1,说明孩子插入在父亲的左子树上,导致父亲的左子树变高,所以需要继续往上更新,往上迭代的方式:先更新cur结点,再通过cur结点算其父亲。
  • 更新后父亲的平衡因子是2或者-2,此时树的平衡因子不符合AVL树的规则,需要进行调整(旋转),下面重点介绍四种旋转方式。
  • 左旋:适用于旋转的子树是整体是右边比较高,首先需要先保存一些关键的结点指针,分别是通过parent计算得出的爷爷结点(pparent),右子树(subR),右子树的左孩子(subRL),旋转的过程中需要注意两个细节:parent可能是整棵树中的某一棵子树,也可能是整棵树的根节点subRL不一定存在在将parent->_right指向subRL之后,需要更新subRL->_parent,但是subRL不一定存在,所以此时需要先判断一下如果parent是根节点,则旋转之后需要更新根节点为subR,并且将subR->_parent指向nullptr。如果parent不是根,则此时pparent结点就会发挥作用,需要先判断parent是pparent的左孩子还是右孩子,以确定是让pparent->_left还是pparent->_right托管subR,然后再更新sub->_parent指向pparent。最后还需要更新平衡因子,左旋的过程中动的只是parent和subR的孩子,所以只需要更新这两个结点,并且左旋的结果是使得它们均平衡,所以最终的平衡因子都是0。
  • 右旋:右旋和左旋是非常类似的,首先需要先保存一些关键的结点指针:通过parent计算得出的pparent(parent的父亲),subL(parent的左孩子),subLR(subL的右孩子)。旋转的过程中同样需要注意两个细节:parent可能是整棵树的根节点,也可能只是某一棵子树的根节点subLR可能存在也可能不存在如果subLR不存在,则不需要更新subRL->_parent判断parent如果parent是整棵树的根节点,则需要更新根节点为subL,然后将根节点的父亲指向空指针,如果parent不是根节点,则此时pparent就会起作用,此时判断parentpparent->_left还是pparent->_right,以决定让pparent->_left还是pparent->_right托管subL,然后再更新subL->_parent指向pparent。最后更新平衡因子,和左旋过程类似,该过程中只是动了parentsubL的孩子,所以只需要更新这两个结点,并且最终都是平衡,所以最终的平衡因子都是0。
  • 左右双旋:这个双旋适合于parent的右子树高,cur的左子树高的情况,先保存一些关键的结点:subR(parent->_right),subRL(subR->_left)旋转的过程,首先让cur右旋,使整棵子树保持右边高,然后再让parent进行左旋。双旋之后,subRL会到根的位置,parent到根的左子树,subR到根的右子树(可以通过画图进行分析),最后调整平衡因子,调整平衡因子需要根据旋转前的subRL的平衡因子(保存为bf)的情况进行分类讨论。如果bf == 0,则subRL是新插入的结点,此时parent,subR,subRL的平衡因子都更新成0。 如果bf == -1,则新节点插入在subRL的左子树,旋转过程中这个新节点会分给左边(parent),所以左边的平衡因子会被平衡故最终的平衡因子为:parent->_bf = 0,subR->_bf = 1,subRL->_bf = 0。如果bf == 1,则新结点插入在subR的右子树,旋转过程中,这个新节点会分给右边(subR),所以最终右边的平衡因子会被平衡,故最终的平衡因子为:parent->_bf = -1,subR->_bf = 0,subRL->_bf = 0
  • 右左双旋:这个双旋适合于parent的左子树高,cur的右子树高的情况,先保存一些关键的结点:subL(parent->_left),subLR(parent->_left->_right),旋转过程:先对subL进行左旋,转化为整体的左子树比较高,再对整体进行右旋,使整棵树保持相对平衡。旋转的结果:subLR到根的位置,parent到根的右子树,subL到根的左子树,再通过旋转前subLR的平衡因子(保存为bf)进行分类讨论:如果bf == 0,则说明subLR是新插入的结点,最终的平衡因子:parent->_bf = 0,subL->_bf = 0,subLR->_bf = 0,如果bf == -1,则说明新结点插入在subLR的左边,这个新结点在旋转的过程中会分给左边(subL),所以左边的平衡因子会被平衡,故最终的平衡因子为:parent->_bf = 1,subL->_bf = 0,subLR->_bf = 0,如果bf == 1,说明新插入的结点插入在subLR的右边,这个新节点在旋转的过程中会分给右边(parent),右边的平衡因子会被平衡,所以最终的平衡因子为:parent->_bf = 0,subL->_bf = -1,subLR->_bf = 0

下面给出上述讲解过程中需要用到的操作的代码:

  • 左旋
// 左旋
	void RotateL(Node* parent)
	{
		// 保存一些关键的结点指针
		Node* pparent = 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是根
			_root = subR;
			_root->_parent = nullptr;
		}
		else
		{
			// parent不是根
			if (pparent->_left == parent)
			{
				pparent->_left = subR;
			}
			else
			{
				pparent->_right = subR;
			}
			subR->_parent = pparent;
		}

		// 更新平衡因子
		parent->_bf = 0;
		subR->_bf = 0;
	}
  • 右旋
// 右旋
	void RotateR(Node* parent)
	{
		// 保存一些关键结点
		Node* pparent = parent->_parent;
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

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

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

		// 判断parent是否为根
		if (parent == _root)
		{
			// parent为根
			_root = subL;
			_root->_parent = nullptr;
		}
		else
		{
			// parent不是根
			if (pparent->_left == parent)
			{
				pparent->_left = subL;
			}
			else
			{
				pparent->_right = subL;
			}
			subL->_parent = pparent;
		}

		// 更新平衡因子
		parent->_bf = 0;
		subL->_bf = 0;

	}
  • 左右双旋
// 先左旋再右旋
	void RotateLR(Node* parent)
	{
		// 先保存一些关键信息
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		// 旋转前先保存subLR的平衡因子
		int bf = subLR->_bf;

		// 旋转
		RotateL(subL);
		RotateR(parent);

		// 更新平衡因子
		if (bf == 0)
		{
			// subLR就算新插入的结点
			parent->_bf = 0;
			subLR->_bf = 0;
			subL->_bf = 0;
		}
		else if (bf == -1)
		{
			// 新插入的结点插入在subLR的左边
			parent->_bf = 1;
			subLR->_bf = 0;
			subL->_bf = 0;
		}
		else if (bf == 1)
		{
			// 新插入的结点插入在subLR的右边
			parent->_bf = 0;
			subL->_bf = -1;
			subLR->_bf = 0;
		}
		else
		{
			// 其他错误
			assert(false);
		}
	}
  • 右左双旋
// 先右旋再左旋
	void RotateRL(Node* parent)
	{
		// 保存一些关键信息
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		// 保存subRL的平衡因子
		int bf = subRL->_bf;

		// 旋转
		RotateR(subR);
		RotateL(parent);

		// 更新平衡因子
		if (bf == 0)
		{
			// subRL是新插入的结点
			parent->_bf = 0;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == -1)
		{
			// 新插入的结点插入在subRL的左边
			parent->_bf = 0;
			subR->_bf = 1;
			subRL->_bf = 0;
		}
		else if (bf == 1)
		{
			// 新插入的结点插入在subRL的右边
			parent->_bf = -1;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else
		{
			// 其他错误
			assert(false);
		}
	}

三、判断平衡树的方法

思路:采用递归的方法,先判断当前树是否为平衡树(本节采用平衡因子进行判断),如果当前为平衡树,则继续递归判断左子树和右子树是否为平衡树

  • 代码:
// 求树的高度的子函数
	size_t _TreeHeight(Node* root)
	{
		if (root == nullptr)
		{
			// 空树
			return 0;
		}

		// 非空树
		// 先算左子树的高度
		size_t LeftHeight = _TreeHeight(root->_left);

		// 再算右子树的高度
		size_t RightHeight = _TreeHeight(root->_right);

		return LeftHeight > RightHeight ? LeftHeight + 1 : RightHeight + 1;
	}

// 判断平衡树的子函数
	bool _IsBalanceTree(Node* root)
	{
		if (root == nullptr)
		{
			// 空树
			return true;
		}

		// 先算左子树高度
		size_t LeftHeight = _TreeHeight(root->_left);
		// 再算右子树高度
		size_t RightHeight = _TreeHeight(root->_right);

		// 计算平衡因子
		int bf = RightHeight - LeftHeight;

		if (abs(bf) > 1)
		{
			cout << "树失衡,不断平衡树" << endl;
			return false;
		}

		if (bf != root->_bf)
		{
			cout << "平衡因子于实际不符合,不是平衡树" << endl;
			return false;
		}

		// 递归判断左右子树
		return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
	}

	// 判断平衡树
	bool IsBalanceTree()
	{
		return _IsBalanceTree(_root);
	}
  • 8
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值