AVL树【概念 + 模拟实现】

概念

二叉搜索树在有序或者接近有序的情况下二叉搜索树退化为单支树(或者类似单支),其平均比较次数为: O ( N 2 ) O(\frac{N}{2}) O(2N)

因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:

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

因此AVL树也是由这两位数学家的名字命名的:

AVL树和空树都是具有以下性质的二叉搜索树:

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

在这里插入图片描述

一颗高度平衡的二叉搜索树可以就是AVL树,其高度和搜索效率都能保证在 O ( l o g 2 n ) O(log_2^n) O(log2n)

AVL树的节点

需要记录左右子树的高度差(即平衡因子),新插入的节点的平衡因子是0,因为AVL树涉及向上更新平衡因子,所以记录父节点。

template<typename K, typename V>
struct AVLNode
{
	AVLNode(const pair<K, V>& kv)
		: _kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
	{}

	AVLNode<K, V>* _left;
	AVLNode<K, V>* _right;
	AVLNode<K, V>* _parent;
	pair<K, V> _kv;
	int _buf = 0;
};

AVL的插入(重头戏)

空树插入

// 空树的插入
if (_root == nullptr)
{
	_root = new Node(kv);
	return true;
}

正常插入

正常插入前需要先找到插入的位置,如果存在该节点即插入失败,走到NULL就是需要插入的位置,同时需要记录父节点:

// 非空树的插入
Node* parent = nullptr;
Node* cur = _root;

// 寻找插入的位置
while (cur)
{
	parent = cur;

	if (kv > cur->_kv)
	{
		cur = cur->_right;
	}
	else if (kv < cur->_kv)
	{
		cur = cur->_left;
	}
	else	// 找到重复元素,插入失败
	{
		return false;
	}
}

// 开始插入
cur = new Node(kv);
if (kv > parent->_kv)
{
	cur->_parent = parent;
	parent->_right = cur;
}
else
{
	cur->_parent = parent;
	parent->_left = cur;
}

更新平衡因子

新节点插入后要保证AVL树的特性,平衡因子就是衡量的标准:

cur插入后,parent的平衡因子一定需要调整,在插入之前,parent
的平衡因子分为三种情况:-1,0, 1, 分以下两种情况:

  1. 如果cur插入到parent的左侧,只需给parent的平衡因子-1即可
  2. 如果cur插入到parent的右侧,只需给parent的平衡因子+1即可

此时:parent的平衡因子可能有三种情况:0,正负1, 正负2

  1. 如果parent的平衡因子为0,说明插入之前parent的平衡因子为正负1,插入后被调整成0,此时满足AVL树的性质,插入成功
  2. 如果parent的平衡因子为正负1,说明插入前parent的平衡因子一定为0,插入后被更新成正负1,此时以parent为根的树的高度增加,需要继续向上更新
  3. 如果parent的平衡因子为正负2,则parent的平衡因子违反平衡树的性质,需要对其进行旋转处理
while (parent)
{
	// 1. 更新平衡因子
	if (parent->_left == cur)
		parent->_buf--;
	else
		parent->_buf++;

	// 2. 根据parent的平衡因子进行分别处理
	if (parent->_buf == 0)	// 插入成功
	{
		break;
	}
	else if (abs(parent->_buf) == 1)	// 继续往上更新
	{
		cur = parent;
		parent = parent->_parent;
	}
	else if (abs(parent->_buf) == 2)	// 不平衡开始旋转
	{
		.....
		break;
	}
	else	// 说明之前插入的节点有问题
	{
		assert(false);
	}
}

AVL树的旋转

旋转分为四种:

在这里插入图片描述

总结:中间挑两边,单旋都为0,双旋看中间,中间为0即为0,非0双取反(方向和值)

代码:

if (parent->_buf == 2 && cur->_buf == 1)	// 单右侧高,左单旋
{
	RotateL(parent);
}
else if (parent->_buf == -2 && cur->_buf == -1)		// 单左侧高, 右单旋
{
	RotateR(parent);
}
else if (parent->_buf == -2 && cur->_buf == 1)	// 左括号型,左右双旋(先左旋转subL转换为单左侧高,再右旋转parent)
{
	RotateLR(parent);
}
else if (parent->_buf == 2 && cur->_buf == -1)	// // 右括号型,右左双旋(先右旋转subR转换为单左侧高,再左旋转parent)
{
	RotateRL(parent);
}
else
{
	assert(false);
}

左单旋

在这里插入图片描述

// 左单旋
void RotateL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	Node* pparent = parent->_parent;

	parent->_right = subRL;
	if (subRL)
		subRL->_parent = parent;

	parent->_parent = subR;
	subR->_left = parent;

	subR->_parent = pparent;
	if (pparent)
	{
		if (pparent->_left == parent)
			pparent->_left = subR;
		else
			pparent->_right = subR;	
	}
	else 
		_root = subR;


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

右单旋

在这里插入图片描述

// 右单旋
void RotateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	Node* pparent = parent->_parent;


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

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

	subL->_parent = pparent;
	if (pparent)
	{
		if (pparent->_left == parent)
			pparent->_left = subL;
		else
			pparent->_right = subL;
	}
	else
		_root = subL;


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

左右双旋

在这里插入图片描述

// 左右双旋
void RotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	int buf = subLR->_buf;
	
	RotateL(subL);
	RotateR(parent);

	subLR->_buf = 0;
	if (buf == -1)
	{
		subL->_buf = 0;
		parent->_buf = 1;
	}
	else if (buf == 1)
	{
		parent->_buf = 0;
		subL->_buf = -1;
	}
	else if (buf == 0)
	{
		parent->_buf = subL->_buf = 0;
	}
	else
	{
		assert(false);
	}

}

右左双旋

在这里插入图片描述

// 右左双旋
void RotateRL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	int buf = subRL->_buf;
		
	RotateR(subR);
	RotateL(parent);
	
	subRL->_buf = 0;
	if (buf == 1)
	{
		parent->_buf = -1;
		subR->_buf = 0;
	}
	else if (buf == -1)
	{
		parent->_buf = 0;
		subR->_buf = 1;
	}
	else if (buf == 0)
	{
		parent->_buf = subR->_buf = 0;
	}
	else
	{
		assert(false);
	}
}

AVL树的验证

AVL树的验证需要验证两步:

  1. 符合二叉搜索树的特性(中序遍历是有序的)
  2. 左右子树高度差不超过1

第一步很好验证,只要按照二叉搜索树进行插入就不会出错,基本上不需要刻意验证。但是第二步需要单独验证一下:

bool IsBalance()
{
	return _IsBalance(_root);
}

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

	int leftH = Hight(root->_left);
	int rightH = Hight(root->_right);
	int diff = abs(leftH - rightH);
	
	if (diff > 2)
		return false;

	return _IsBalance(root->_left) && _IsBalance(root->_right);
}

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

	return max(Hight(root->_left), Hight(root->_right)) + 1;
}

AVL树的删除

因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不过与二叉搜索树不同的是,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。

有兴趣参考这篇博客:二叉树的删除

AVL树性能分析

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这
样可以保证查询时高效的时间复杂度,即 l o g 2 ( N ) log_2 (N) log2(N)

但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。

因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_featherbrain

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值