【数据结构】AVL树

AVL树的概念

二叉搜索树查找效率很高,但是当但数据有序接近有序二叉搜索树退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下

为了解决上面的问题,就得优化二叉搜索树,让二叉搜索树的高度不要太高,接近平衡(左右子树高度差不要太多)

于是,两位俄罗斯的数学家G.M.Adelson-VelskiiE.M.Landis1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度

所以AVL树也叫做高度平衡搜索树


AVL树的性质

AVL树其实就是特殊的二叉搜索树

它的特点是:

  • 本身首先是一棵二叉搜索树
  • 每个结点的左右子树的高度之差的绝对值(平衡因子)不超过1(-1/0/1)

也就是说,AVL树,本质上是带了平衡功能二叉查找树

在这里插入图片描述

如果它有n个结点,其高度可保持在O(log2N)搜索时间复杂度O(log2N)


AVL树的实现

AVL树的实现有多种方式,这里采用三叉链平衡因子来完成平衡的功能

AVL树节点

采用KV结构存储键值对数据,节点中还包括平衡因子(右子树高度-左子树高度),parent指针左子树指针右子树指针,还有节点的构造函数

template<class K, class V>
struct AVLTreeNode {
	//三叉链
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;

	//平衡因子 Balance factor
	int _bf;

	//存储键值对
	pair<K, V> _kv;
public:
    //平衡因子初始化为0
	AVLTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
		,_kv(kv)
	{}
};

AVL树的插入

AVL树是引入了平衡因子的二叉搜索树,所以AVL树的插入分为三个部分

  • 按照二叉搜索树的插入方式插入新节点
  • 更新平衡因子,并检测是否破坏了AVL树的平衡性质
  • 如果平衡因子等于2或者-2,说明树的平衡已经出现了问题,需要进行旋转处理

插入新结点

按照二叉搜索树的插入方式

  • 树为空,创建一个结点,直接链接到根结点
  • 树不为空,按照二叉搜索树的性质找到插入的位置,再创建结点插入链接

平衡因子的更新和检测

插入节点后,节点所对应的父节点的高度可能会发生变化,所以需要对新节点的父节点的平衡因子进行更新,以便于检测树的平衡性

而往往一个节点的子树高度发生变化后可能也会影响到祖先节点子树的高度变化,所以平衡因子的更新可能需要不断向上更新

平衡因子的更新规则

  • 新增的结点在parent的左边,parent的平衡因子--
  • 新增的结点在parent的右边,parent的平衡因子++

平衡因子的检测

由于插入新结点可能会引起祖先节点的平衡因子变化,也可能会导致树的不平衡,所以在更新完新增节点parent的平衡因子后,还需要对新增节点到根节点路径上的所有节点的平衡因子进行检测

  • parent的平衡因子等于0,则插入成功
  • parent的平衡因子等于1或者-1,还需要继续往上更新平衡因子
  • parent的平衡因子等于2或者-2,此时以parent结点为根结点的子树已经不平衡了需要进行旋转处理

AVL树的旋转

parent的平衡因子是2或者-2时,此时二叉搜索树已经不平衡了,需要对这个树进行旋转操作,让这个树重新平衡

AVL树的旋转分为四种左单旋右单旋左右双旋右左双旋

平衡因子的四种情况分别对应四种旋转

  • parent的平衡因子为2,说明parent的右子树高,设parent的右子树为subR
    • subR的平衡因子为1,执行左单旋
    • subR的平衡因子为-1,执行右左双旋
  • parent的平衡因子为-2,说明parent的左子树高,设parent的左子树为subL
    • subR的平衡因子为-1,执行右单旋
    • subR的平衡因子为1,执行左右双旋

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


左单旋

如果新增的节点插入到较高右子树的右侧,就要进行左单旋

左单旋的步骤:

  • subRL作为parent的右子树
  • parent作为subR的左子树
  • 如果parent为根结点,则让zubR作为新的根结点
  • 如果parent不是根节点,则维护subRparent父亲结点链接关系
  • 更新平衡因子

在这里插入图片描述

void _RotateL(Node* parent) {
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
    	//保存根节点的父节点,用来维护改变后的节点关系
		Node* grandParent = parent->_parent;
		
    	//让subRL作为parent的右子树
		parent->_right = subRL;
		if (subRL) {
			subRL->_parent = parent;
		}
		
    	//让parent作为subR的左子树
		subR->_left = parent;
		parent->_parent = subR;
		
    	//检测parent是否为根节点
		if (parent == _root) {
			_root = subR;
			subR->_parent = nullptr;
		}
		else {
			if (grandParent->_left == parent) {
				grandParent->_left = subR;
			}
			else {
				grandParent->_right = subR;
			}
			subR->_parent = grandParent;
		}

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

右单旋

如果新增的节点插入到较高左子树的左侧,就要进行右单旋

右单旋的步骤和代码和左单旋相似,可参考完成

在这里插入图片描述


左右双旋

如果新增节点插入较高左子树的右侧,就要进行左右单旋先进行左单旋,再进行右单旋

左右双旋的步骤:

  • 以subL为旋转点进行左单旋
  • 以parent为旋转点进行右单旋
  • 更新平衡因子

在这里插入图片描述


左右双旋平衡因子的更新分为三种情况

  • 新增的结点在subLR的右侧,也就是subLR的平衡因子为1

在这里插入图片描述

进行左右双旋后,parentsubLsubLR的平衡因子分别更新为0-10

  • 新增的节点在subLR左侧,也就是subLR的平衡因子为-1

在这里插入图片描述

进行左右双旋后,parentsubLsubLR的平衡因子分别更新为100

  • 新增的节点就是subLR,也就是subLR的平衡因子为0

在这里插入图片描述

进行左右双旋后,parentsubLsubLR的平衡因子分别更新为000

左右双旋代码如下

void _RotateLR(Node* parent) {
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	int bf = subLR->_bf;

	_RotateL(subL);
	_RotateR(parent);

	//更新平衡因子
	if (bf == 1) {
		//插入的新结点在subLR右边
		subLR->_bf = 0;
		subL->_bf = -1;
		parent->_bf = 0;
	}
	else if (bf == -1) {
		//插入的新结点在subLR的左边
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 1;
	}
	else if (bf == 0) {
		//插入的新结点就是subLR
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 0;
	}
}

右左双旋

右左双旋的步骤:

  • 以subR为旋转点进行右单旋
  • 以parent为旋转点进行左单旋
  • 更新平衡因子

在这里插入图片描述


右左双旋平衡因子的更新分为三种情况

  • 新增的结点在subRL的右侧,也就是subRL的平衡因子为1

在这里插入图片描述

进行右左双旋后,parentsubRsubRL的平衡因子分别更新为-100

  • 新增的结点在subRL的左侧,也就是subRL的平衡因子为-1

在这里插入图片描述

进行右左双旋后,parentsubRsubRL的平衡因子分别更新为010

  • 新增的结点就是subRL,也就是subRL的平衡因子为0

在这里插入图片描述

进行右左双旋后,parentsubRsubRL的平衡因子分别更新为000

右左双旋代码如下

void _RotateRL(Node* parent) {
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	int bf = subRL->_bf;

	_RotateR(subR);
	_RotateL(parent);

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

AVL树insert实现代码
pair<Node*, bool> Insert(const pair<K, V>& kv) {
	//按照二叉搜索树的方式插入新节点

	//_root 为空,直接插入
	if (_root == nullptr) {
		_root = new Node(kv);
		return make_pair(_root, true);
	}
			
	//_root不为空,找到插入位置再插入
	Node* parent = _root;
	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 {
			//key已经存在,插入失败
			return make_pair(cur, false);
		}
	}

	//此时cur为空,已经找到插入位置	
	cur = new Node(kv);
    //记录新插入的结点
	Node* newNode = cur;

	if (parent->_kv.first > cur->_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++;
		}

		//对AVL树的平衡性进行检查
		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) {
			//树已经不平衡,旋转
			if (parent->_bf == -2) {
				if (cur->_bf == -1) {
					//右单旋
					_RotateR(parent);
				}
				else { //cur->_bf == 1
					//左右双旋
					_RotateLR(parent);
				}				
			}
			else { //parent->_bf == 2
				if (cur->_bf == 1) {
					//左单旋
					_RotateL(parent);
				}
				else { //cur->_bf == -1
					//右左双旋
					_RotateRL(parent);
				}
			}
			//旋转完退出
			break;
		}
		else {
			//出错
			assert(false);
		}
	}
	return make_pair(newNode, true);
}

AVL树的查找

AVL树的查找逻辑和二叉搜索树一样

  • 若树为空查找失败,返回nullptr
  • key值小于当前节点,就去当前节点的左子树查找
  • key值大于当前节点,就去当前节点的右子树查找
  • key值等于当前节点查找成功,返回对应节点指针
Node* Find(const K& key) {
	Node* cur = _root;

	while (cur) {
		if (cur->_kv.first > key) {
			cur = cur->_left;
		}
		else if (cur->_kv.first < key) {
			cur = cur->_right;
		}
		else {
			return cur;
		}
	}

	return nullptr;
}

AVL树value的修改

AVL树的key不能修改,只能通过key修改对应的value值

可以对运算符[]进行重载

insert的返回值是一个键值对,键值对的first值是成功插入结点的指针,或者已经存在的结点的指针

可以调用insert函数用来获取想要修改的key所对应的value

当使用 对象[key]

  • 如果key不在树中先插入键值对<key, V()>,然后返回该键值对中value的引用
  • 如果key已经存在返回键值为key结点value的引用
V& operator[](const K& key) {
	pair<Node*, bool> ret = Insert(make_pair(key, V()));
	return ret.first->_KV.second;
}

AVL树的验证

可以用中序遍历是否有序的方式来验证AVL树的搜索性质

//中序遍历
void InOrde() {
	_InOrder(_root);
}

void _InOrder(Node* root) {
	if (root == nullptr) {
		return;
	}

	_InOrder(root->_left);
	cout << root->_kv.first << " : " << root->_kv.second << endl;
	_InOrder(root->_right);

}

这种方式验证了AVL树的搜索性质,但验证不了它的平衡性

验证平衡性,需要再进行判断,在验证平衡性时,顺便验证AVL树的平衡因子

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

//计算树的高度
int _HeightT(Node* root) {
	if (root == nullptr) {
		return 0;
	}

	int leftHeight = _HeightT(root->_left);
	int rightHeight = _HeightT(root->_right);

	return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

//验证树的平衡性
bool _IsBalance(Node* root) {
	if (root == nullptr) {
		return true;
	}

	int leftHeight = _HeightT(root->_left);
	int rightHeight = _HeightT(root->_right);	
    
    //判断平衡因子是否符合AVL树的要求
	if (rightHeight - leftHeight != root->_bf) {
		cout << "平衡因子异常" << root->_kv.first << endl;
		return false;
	}
		
	return abs(rightHeight - leftHeight) < 2
		&& _IsBalance(root->_left)
		&& _IsBalance(root->_right);
}

AVL树的删除

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

具体实现参考

2021dragon《AVL树(动图详解)》


AVL树的性能

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


完整代码实现

AVLTree.h

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

xiaomage1213888

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

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

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

打赏作者

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

抵扣说明:

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

余额充值