<C++> AVLTree

目录

一、AVL树概念

1.二叉搜索树的缺点 

2.AVL树的概念 

二、AVL树定义

三、AVL树插入 

1.插入节点

2.控制平衡 

(1)更新平衡因子

(2)旋转

①左单旋

①右单旋

 ③左右双旋

④右左双旋

四、AVL树高度 

五、判断是否为AVL树

六、AVL树的性能


一、AVL树概念

1.二叉搜索树的缺点 

我们知道 map/multimap/set/multiset 这些容器底层都按照二叉搜索树实现,但是二叉搜索树的缺点在于,假如向树中插入的元素有序或者接近有序时,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),相当于在顺序表中搜索元素,效率低下。所以map/multimap/set/multiset的底层结构对二叉搜索树做了处理,采用平衡树来实现。

2.AVL树的概念 

如何避免二叉树搜索树会退化成单支树的缺点呢?

向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。为什么高度差的绝对值不超过1而不是0呢?因为如果高度差的绝对值不超过0,那么二叉树就变成满二叉树了,因此绝对值不能超过1。这就引入了平衡二叉树的概念:

一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

(1)它的左右子树都是AVL树
(2)左右子树高度之差(简称平衡因子=右子树高度-左子树高度)的绝对值不超过1(-1/0/1)

如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 O(),搜索时间复杂度O()。 


二、AVL树定义

由于要实现AVL树的增删改查,所以定义AVL树的节点,就需要定义parent,否则插入节点时,不知道要链接到树里面哪个节点下面。

template<class K, class V>
class 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树插入 

1.插入节点

插入节点需要先判断树是否为空:

(1)若为空,让该节点作为根节点

(2)若不为空,分3种情况:

①key比当前节点小,向左走

②key比当前节点大,向右走

③相等,插入失败

如果没找到节点,那么需要插入新节点

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

		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first > kv.first)// 往左找
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_kv.first < kv.first)// 往右找
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return false; // 值相等
			}
		}

		cur = new Node(kv);
		if (parent->_kv.first > kv.first)
			parent->_left = cur;
		else
			parent->_right = cur;

		cur->_parent = parent; // 反向链接父亲

        
        // 控制平衡,更新平衡因子
        // ...


        return true;
    }

2.控制平衡 

(1)更新平衡因子

一个节点的平衡因子是否需要更新,取决于它的左右子树的高度是否发生变化。如果插入节点后,它的父节点到根节点路径上的部分节点的平衡因子发生改变,那么需要对这些节点进行更新,以保持树的平衡。因此

①如果新增节点是父亲的左子树(cur == parent->left),那么parent->_bf--

②如果新增节点是父亲的右子树(cur == parent->right),那么parent->_bf++

更新后:

什么决定了是否要继续往上更新爷爷节点,取决于parent所在的子树高度是否变化?变了继续更新,不变则不再更新。
        a、parent->bf == 1 ||  parent->bf == -1  --》parent所在的子树变了,继续更新,为什么?
 =》说明插入前parent->bf == 0,说明插入前左右两边高度相等,现在有一边高1,说明                  parent 一边高一边低,高度变了。

        b、parent->bf == 2 || parent->bf == -2  --》parent所在的子树不平衡,需要处理这颗子树(旋      转处理)。

        c、 parent->bf == 0, parent所在的子树高度不变,不用继续网上更新。这一次插入结束,为    什么呢?
        =》说明插入前是parent->bf == 1 or -1,插入之前一边高,一边低,插入节点填上矮的边,              它的高度不变。

        // 控制平衡,更新平衡因子
		while (parent)
		{
			if (parent->_bf == 1 || parent->_bf == -1)
			{
				// 继续更新
				parent = parent->_parent;
			}
			else if (parent->_bf == 0)
			{
				break;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				// 需旋转处理 -- 1、让这颗子树平衡 2、降低这颗子树的高度
				if (parent->_bf == 2 && cur->_bf == 1)
				{
					RotateL(parent);// 左单旋
				}
				else if (parent->_bf == -2 && cur->_bf == -1)
				{
					RotateR(parent);// 右单旋
				}
				else if ()
				{

				}
					
			}
			else
			{
				assert(false); // 插入前这棵树已经有了问题
			}
		}

(2)旋转

旋转处理有4种:右单旋、左单旋、右左单旋、左右单旋

左单旋

新节点插入到较高右子树的右侧,即右右-----左单旋

 插入新节点前,AVL树是平衡的,新节点插入到60的右子树,那么60的右子树增加了一层,导致以30为根的二叉树不平衡。为了让30平衡,只能让30的右子树的高度减小一层,并把60的左子树的高度增加一层。

因此,要把60的右子树往上提,把30转下来,因为30比60小,只能把30放在60的左子树,60的左子树比60小,比30大,因此只能把60的左子树放在30的右子树。再更新节点平衡因子。

抽象图:

 需要考虑的情况:

(1)60的左孩子可能存在,也可能不存在

(2)30可能是根节点,也可能是子树;如果是根节点,旋转后,要更新根节点。如果是子树,可能是左子树也可能是右子树,就把30原来的父亲的左或右指向60。

	// 左单旋
	void RotateL(Node* parent)
	{
		Node* childR = parent->_right;
		Node* childRL = childR->_left;


		parent->_right = childRL;
		if (childRL)
			childRL->_parent = parent; // 更新父亲

		Node* ppnode = parent->_parent;

		childR->_left = parent;
		parent->_parent = childR; // 更新父亲

		// 是一颗单独的树
		if (ppnode == nullptr)
		{
			_root = childR;
			_root->_parent = nullptr;
		}
		else 
		{
			// 是一颗子树,判断是左子树还是右子树
			if (ppnode->_left == parent)
			{
				ppnode->_left = childR;
			}
			else
			{
				ppnode->_right = childR;
			}
			
			childR->_parent = ppnode;// 更新父亲
		}
		parent->_bf = childR->_bf = 0; // 更新平衡因子
	}

 具象图:

h=0的情况:

例子:20变成10的左子树,10的左子树为空,不考虑

h=1的情况:

例子:10变成20的左子树,20的左子树变成10的右子树

h=2的情况: 

例子:10变成20的左子树,20的左子树变成10的右子树

 h == ...多种情况就不一一列举了。

①右单旋

将新节点插入到较高左子树的左侧,即左左-----右单旋

 插入新节点前,AVL树是平衡的,新节点插入到30的左子树,那么30的左子树增加了一层,导致以60为根的二叉树不平衡。为了让30平衡,只能让30的左子树的高度减小一层,并把60的右子树的高度增加一层。

因此,要把30的左子树往上提,把60转下来,因为60比30大,只能把60放在30的右子树,30的右子树比30大,比60小,因此只能把30的右子树放在60的左子树。再更新节点平衡因子。

抽象图:
需要考虑的情况:

(1)30的右孩子可能存在,也可能不存在

(2)60可能是根节点,也可能是子树;如果是根节点,旋转后,要更新根节点。如果是子树,可能是左子树也可能是右子树,就把60原来的父亲的左或右指向30。

	// 右单旋
	void RotateR(Node* parent)
	{
		Node* childL = parent->_left;
		Node* childLR = childL->_right;


		parent->_left = childLR;
		if (childLR)
			childLR->_parent = parent; // 更新父亲

		Node* ppnode = parent->_parent;

		childL->_right = parent;
		parent->_parent = childL; // 更新父亲

		// 是一颗单独的树
		if (parent == _root)
		{
			_root = childL;
			_root->_parent = nullptr;
		}
		else
		{
			// 是一颗子树,判断是左子树还是右子树
			if (ppnode->_left == parent)
			{
				ppnode->_left = childL;
			}
			else
			{
				ppnode->_right = childL;
			}

			childL->_parent = ppnode;// 更新父亲
		}
		parent->_bf = childL->_bf = 0; // 更新平衡因子
	}

具象图:模板就不画了,根据左单旋照葫芦画瓢,举样例

h=0的情况:

20变成10的左子树,10的左子树为空,不用考虑 

h=1的情况:

20变成10的右子树,10的右子树12变成20的左子树

h=2的情况:

 20变成10的左子树,10的右子树12变成20的左子树

 ③左右双旋

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

插入新节点前,AVL树是平衡的,新节点插入到60的左子树,那么60的左子树增加了一层,导致以90为根的二叉树不平衡。为了让90平衡,只能让90的左子树的高度减小一层。

现在90左子树的高度是h+1,但是现在不能把30进行右单旋,因为要把30右单旋,那么60和90都必须处于30的右子树,这办不到。因此要分为两步:

(1)先把30左单旋

(2)再把60右单旋

 

void RotateLR(Node* parent)
	{
		Node* childL = parent->_left;
		Node* childLR = childL->_right;

		int bf = childLR->_bf; 
		//旋转之前,保存childLR的平衡因子,旋转完成之后,需要根据该平衡因子来调整其他节点的平衡因子

		RotateL(parent->_left);
		RotateR(parent);

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

④右左双旋

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

插入新节点前,AVL树是平衡的,新节点插入到60的右子树,那么60的右子树增加了一层,导致以30为根的二叉树不平衡。为了让30平衡,只能让30的右子树的高度减小一层。 

现在30右子树的高度是h+1,但是现在不能把60进行左单旋,因为要把60左单旋,那么30和90都必须处于60的左子树,这办不到。因此要分为两步:

(1)先把90右单旋

(2)再把30左单旋

 

//右左单旋
	void RotateRL(Node* parent)
	{
		Node* childR = parent->_right;
		Node* childRL = childR->_right;

		int bf = childLR->_bf;
		//旋转之前,保存childLR的平衡因子,旋转完成之后,需要根据该平衡因子来调整其他节点的平衡因子

		RotateR(parent->_right);
		RotateL(parent);

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

四、AVL树高度 

计算树高度肯定要递归计算:

(1)计算左右子树的高度

(2)谁的高度大,那AVL树的高度就为较高子树的高度+1

int _Height(Node* root)
{
	if (root == nullptr)
		return 0;

	int leftH = _Height(root->_left);
	int rightH = _Height(root->_right);

	return leftH > rightH ? leftH + 1 : rightH + 1;
}

五、判断是否为AVL树

检查树是否是AVL树:

如何检验自己的 AVL 树是否合法? 答案是通过平衡因子检查

平衡因子 反映的是 左右子树高度之差,计算出 左右子树高度之差 与当前节点的 平衡因子 进行比对,如果发现不同,则说明 AVL 树 非法

或者如果当前节点的 平衡因子 取值范围不在 [-1, 1] 内,也可以判断 非法

(1)获取左右子树高度

(2)根据左右子树高度计算平衡因子

(3)当平衡因子<=2 || -2时就是平衡的

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

		int leftH = _Height(root->_left);
		int rightH = _Height(root->_right);

		if (rightH - leftH != root->_bf)
		{
			cout << root->_kv.first << "节点平衡因子异常" << endl;
			return false;
		}

		// 递归所有子树判断是否平衡
		return abs(leftH - rightH) < 2
			&& _IsBalance(root->_left)
			&& _IsBalance(root->_right);
	}

六、AVL树的性能

AVL 树是一棵 绝对平衡 的二叉树,对高度的控制极为苛刻,稍微有点退化的趋势,都要被旋转调整,这样做的好处是 严格控制了查询的时间,查询速度极快,约为 logN

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值