【数据结构】AVL树

一、AVL树的概念

虽然二叉搜索树可以缩短查找的效率,但是如果数据有序或者接近有序,二叉搜索树就会退化成单支树,就相当于在顺序表中查找,这样效率就很低下。

为了解决上面的问题,就引入了一种规划:在向二叉搜索树中插入新节点后,保证节点的左右子树高度差不超过1,经过这样调整就降低树的高度。减少平均搜索时间。

一颗AVL树具有以下性质:

1.它的左右子树都是AVL树

2.左右子树的高度差的绝对值不超过一。

二、AVL树节点的定义。

template<class K, class V>
struct AVLTreeNode
{
	struct AVLTreeNode* _left;		//左节点
	struct AVLTreeNode* _right;		//右节点
	struct AVLTreeNode* _parent;	//双亲节点
	pair<K, V> _kv;
	int _bf;						//平衡因子

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

三、AVL树的插入

AVL树就是在搜索二叉树的基础上引入了平衡因子。

AVL树的插入分为i两部:

1.按照二叉搜索树的规则插入节点。

2.调整平衡因子

在插入新节点后,该节点的父亲的平衡因子需要调整,插入前父节点的平衡因子有三种情况,-1,0,1.

这三种情况可以以归类为两种情况:

        1.如果插入到父亲的左侧,只需要给父亲的平衡因子-1即可。

        2.如果插入到父亲的右侧,只需要给父亲的平衡因子+1即可。

改完父亲的平衡因子后父亲的平衡因子又可能会出现三种情况:0,1,-2

1.如果父亲的平衡因子是0,那说明插入之前的平衡因子是 ±1 ,插入后被调整为0。

2.如果父亲的平衡因子是 ±1 说明插入之前父亲的平衡因子一定是0,此时就需要继续向上更新。

3.如果父亲的平衡因子是±2,说明父亲的平衡因子是违反AVL树的性质,需要对其进行旋转。

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;
		cur->_parent = parent;
	}
	else
	{
		parent->_right = cur;
		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)
		{
			//。。。。
		}
		else
		{
			assert(false);
		}
		
		 
	}
	return true;

}

四、AVL树的旋转

根据插入位置的不同,旋转可以分为四种。(以下用抽象图表示)

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

插入完成后导致,5节点的平衡因子为-1,需要继续向上更新平衡因子,进而导致6节点需要调整平衡

所以我们需要把左子树往上提,这样6转下来,因为6比5大,只能将其放在5的右子树,而如果5有 右子树,右子树根的值一定大于5,小于6,只能将其放在6的左子树,旋转完成后,更新节点 的平衡因子即可。在旋转过程中,有以下几种情况需要考虑:

1. 5节点的右孩子可能存在也可能不存在。

2. 6可能是根节点也可能是子树,如果是子树,也可能是左子树或者右子树。如果是根节点,旋转完成后需要跟新节点。

调整完成后的情况:

void RotateL(node* parent)
{
	node* subr = parent->_right;
	node* subrl = subr->_left;

	parent->_right = subrl;
	subr->_left = parent;
	if (subrl)
	{
		subrl->_parent = parent;
	}

	node* pparent = parent->_parent;

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

}

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

void RotateR(node* parent)
{
	node* subl = parent->_left;
	node* sublr = subl->_right;


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

	node* pparent = parent->_parent;

	subl->_right = parent;
	parent->_parent = subl;

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

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

void RotateLR(node* parent)
{
	node* subl = parent->_left;
	node* sublr = subl->_right;

	int bf = sublr->_bf;

	RotateL(subl);
	RotateR(parent);

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

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

void RotateRL(node* parent)
{
	node* subr = parent->_right;
	node* subrl = subr->_left;

	int bf = subrl->_bf;

	RotateR(subr);
	RotateL(parent);

	if (0 == bf)
	{
		parent->_bf = subr->_bf = subrl->_bf = 0;
	}
	else if( -1 == bf)
	{
		//在subrl的左子树插入
		parent->_bf = subrl->_bf = 0;
		subr->_bf = 1;
	}
	else if (1 == bf)
	{
		//在subrl的右子树插入
		subr->_bf = subrl->_bf = 0;
		parent->_bf = -1;
	}
	else
	{
		assert(false);
	}
}

总结:

假如以parent为根的子树不平衡,即parent的平衡因子为2或者-2,分以下情况考虑

1. parent的平衡因子为2,说明parent的右子树高,设parent的右子树的根为subr

当subr的平衡因子为1时,执行左单旋

当subr的平衡因子为-1时,执行右左双旋

2. parent的平衡因子为-2,说明parent的左子树高,设parent的左子树的根为subl

当subl的平衡因子为-1是,执行右单旋

当subl的平衡因子为1时,执行左右双旋

旋转完成后,原parent为根的子树个高度降低,已经平衡,不需要再向上更新。

五、AVL树的验证

AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分为两步:

1.每个节点子树的高度差的绝对值不超过1。

2.节点的平衡因子是否计算正确。

int _Hight(node* root)
{
	if (root == nullptr)
	{
		return 0;
	}
	int lefthight = _Hight(root->_left);
	int righthight = _Hight(root->_right);
	return lefthight > righthight ? lefthight + 1 : righthight + 1;
}

bool _IsBalance(const node* root)
{
	if (root == nullptr)
	{
		return true;
	}
	int lefthight = _Hight(root->_left);
	int righthight = _Hight(root->_right);
	if (righthight - lefthight != root->_bf)
	{
		cout << root->_kv.first << "当前平衡因子异常" << endl;
		return false;
	}


	return abs(righthight - lefthight) < 2
		&& _IsBalance(root->_left)
		&& _IsBalance(root->_right);
}

六、AVL树的性能

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这 样可以保证查询时高效的时间复杂度,即$log_2 (N)$。但是如果要对AVL树做一些结构修改的操 作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时, 有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数 据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值