二叉树进阶 ---AVL树的介绍 AVL树插入的4种情况 (详细图解+代码演示)

之前几篇对map/multimap/set/multiset进行了简单的介绍,在其文档介绍中发现,这几个容器有个共同点是:
底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。

不知道大家还记不记得, 之前说map和set都是用红黑树实现的, 我们今天先不看红黑树, 先来看看高度平衡的AVL树




AVL树的介绍

1. 概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。

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

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

因此, 平衡二叉树(AVL树)诞生了


2. 定义

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

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

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

AVL树在二叉搜索树的基础上新增了平衡因子, 平衡因子的绝对值要<=1
平衡因子: 右子树的高度 - 左子树高度

给出一个例子 :
在这里插入图片描述


3. AVL树结点的定义

AVL树采用 数据 + 三叉链 + 平衡因子的结构

//平衡二叉树结点
template<class T>
struct AVLTreeNode {
	//数据
	T _data;
	//左右孩子指针
	AVLTreeNode<T>* _left;
	AVLTreeNode<T>* _right;
	//父节点指针
	AVLTreeNode<T>* _parent;
	//平衡因子
	int _bf;

	AVLTreeNode(const T& val = T())
		: _data(val)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)
	{}
};


AVL树的插入

AVL树的重中之重也就是它的插入操作了, 接下来我们一起来看

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。

那么AVL树的插入过程可以分为两步:

  • 1. 按照二叉搜索树的方式插入新节点
  • 2. 调整节点的平衡因子

1. 二叉搜索树方式插入新节点

与之前二叉搜索树的插入一样, 也是搜索+插入的过程, 只不过要多连接一个parent指针

	bool insert(const T& val) {
		//空树, 创建根节点
		if (_root == nullptr) {
			_root = new Node(val);
			return true;
		}
		
		//搜索: 不包含重复数据, 左子树小于根, 右子树大于根
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur) {
			parent = cur;
			if (val == cur->_data)
				return false;
			else if (val < cur->_data)
				cur = cur->_left;
			else
				cur = cur->_right;
		}
		//循环走完之后, cur走到nullptr, 而parent就是要插入位置的父节点
		
		//创建新节点, 判断在父节点的哪边, 然后插入
		cur = new Node(val);
		if (val < parent->_data)
			parent->_left = cur;
		else
			parent->_right = cur;

		//最后连接parent指针
		cur->_parent = parent;
	}

2. 调整平衡因子

直接给出图解
在这里插入图片描述
在这里插入图片描述

我们先把这部分写出代码, 如下 :

		//更新平衡因子
		//更新范围: 从parent一直到root, 这条路径上的祖先结点
		while (parent) {
			if (parent->_left == cur) {
				//左子树插入, 左边高度+1, 平衡因子-1
				parent->_bf--;
			}
			else {
				//右子树插入, 右边高度+1, 平衡因子+1
				parent->_bf++;
			}

			//判断parent的平衡因子
			if (parent->_bf == 0) {
				//parent结点某一个子树补齐
				//parent的高度并没有发生变化, 所以对上层没有影响, 结束循环
				break;
			}
			else if (parent->_bf == 1 || parent->_bf == -1) {
				//说明插入之前parent的平衡因子是0
				//插入之后+1/-1, 高度发生了变化
				//要继续向上更新
				cur = parent;
				parent = parent->_parent;
			}
			else {
				//平衡因子为2/-2, 要调整avl树, 重新达到平衡
			}

3. AVL树的旋转

右旋

直接给出图解
在这里插入图片描述

下面给出代码:

	//右旋
	void rotateR(Node* parent) {
		//左子树的根结点
		Node* subL = parent->_left;
		//左子树的右子树的根节点
		Node* subLR = subL->_right;
		//parent的父结点
		Node* grandp = parent->_parent;


		//修改6个链接
		//一对一对的写, 比较容易写出来
		subL->_right = parent;
		parent->_parent = subL;
		
		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;

		//如果parent为根节点, 需要更新根节点
		if (parent == _root) {
			_root = subL;
			subL->_parent = nullptr;
		}
		else {
			//链接grandp
			if (grandp->_left == parent)
				grandp->_left = subL;
			else
				grandp->_right = subL;
			subL->_parent = grandp;
		}

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

可以对着代码看图, 更容易理解


左旋

在这里插入图片描述

左旋代码

	//左旋
	void rotateL(Node* parent) {
		//右子树的根节点
		Node* subR = parent->_right;
		//右子树的左子树的根节点
		Node* subRL = subR->_left;
		//parent的父节点
		Node* grandp = parent->_parent;

		//修改6个指针链接
		subR->_left = parent;
		parent->_parent = subR;
		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;

		//判断parent是否为根节点
		if (parent == _root) {
			_root = subR;
			subR->_parent = nullptr;
		}
		else {
			//链接grandp
			if (grandp->_left == parent)
				grandp->_left = subR;
			else
				grandp->_right = subR;
			subR->_parent = grandp;
		}

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

左右双旋

在这里插入图片描述

代码如下 :

else if (parent->_bf == -2 && cur->_bf == 1) {
					//记录subLR的平衡因子
					Node* subLR = cur->_right;
					int bf = subLR->_bf;

					//左边的右边高: 左右双旋
					rotateL(cur);
					rotateR(parent);

					//  cur  subLR  p
					//双旋之后, subLR是新的根, subLR的左子树放在cur的右边, 右子树放在p的左边
					if (bf == -1) {
						//subLR的左边高
						cur->_bf = 0;
						parent->_bf = 1;
					}
					else if (bf == 1) {
						//subLR的右边高
						parent->_bf = 0;
						cur->_bf = -1;
					}
				}

右左双旋

在这里插入图片描述

代码如下 :

else if (parent->_bf == 2 && cur->_bf == -1) {
					Node* subRL = cur->_left;
					int bf = subRL->_bf;
					
					//右边的左边高: 右左双旋
					rotateR(cur);
					rotateL(parent);

					//  p  subRL  cur
					//双旋之后, subRL是新的根, subRL的左子树放在p的右边, 右子树放在cur的左边
					if (bf == -1) {
						//subRL的左边高
						parent->_bf = 0;
						cur->_bf = 1;
					}
					else if (bf == 1) {
						//subRL的右边高
						cur->_bf = 0;
						parent->_bf = -1;
					}
				}

到这里插入接口就结束了, 下面给出完整的插入代码


完整插入代码

	bool insert(const T& val) {
		//空树, 创建根节点
		if (_root == nullptr) {
			_root = new Node(val);
			return true;
		}
		
		//搜索: 不包含重复数据, 左子树小于根, 右子树大于根
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur) {
			parent = cur;
			if (val == cur->_data)
				return false;
			else if (val < cur->_data)
				cur = cur->_left;
			else
				cur = cur->_right;
		}

		cur = new Node(val);
		if (val < parent->_data)
			parent->_left = cur;
		else
			parent->_right = cur;

		//连接parent指针
		cur->_parent = parent;

		//更新平衡因子
		//更新范围: 从parent一直到root, 这条路径上的祖先结点
		while (parent) {
			if (parent->_left == cur) {
				//左子树插入, 左边高度+1, 平衡因子-1
				parent->_bf--;
			}
			else {
				//右子树插入, 右边高度+1, 平衡因子+1
				parent->_bf++;
			}

			//判断parent的平衡因子
			if (parent->_bf == 0) {
				//parent结点某一个子树补齐
				//parent的高度并没有发生变化, 所以对上层没有影响, 结束循环
				break;
			}
			else if (parent->_bf == 1 || parent->_bf == -1) {
				//说明插入之前parent的平衡因子是0
				//插入之后+1/-1, 高度发生了变化
				//要继续向上更新
				cur = parent;
				parent = parent->_parent;
			}
			else {
				//平衡因子为2/-2, 要调整avl树, 重新达到平衡
				if (parent->_bf == -2 && cur->_bf == -1) {
					//左边的左边高: 右旋
					rotateR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == 1) {
					//右边的右边高: 左旋
					rotateL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == 1) {
					//记录subLR的平衡因子
					Node* subLR = cur->_right;
					int bf = subLR->_bf;

					//左边的右边高: 左右双旋
					rotateL(cur);
					rotateR(parent);

					//  cur  subLR  p
					//双旋之后, subLR是新的根, subLR的左子树放在cur的右边, 右子树放在p的左边
					if (bf == -1) {
						//subLR的左边高
						cur->_bf = 0;
						parent->_bf = 1;
					}
					else if (bf == 1) {
						//subLR的右边高
						parent->_bf = 0;
						cur->_bf = -1;
					}
				}
				else if (parent->_bf == 2 && cur->_bf == -1) {
					Node* subRL = cur->_left;
					int bf = subRL->_bf;
					
					//右边的左边高: 右左双旋
					rotateR(cur);
					rotateL(parent);

					//  p  subRL  cur
					//双旋之后, subRL是新的根, subRL的左子树放在p的右边, 右子树放在cur的左边
					if (bf == -1) {
						//subRL的左边高
						parent->_bf = 0;
						cur->_bf = 1;
					}
					else if (bf == 1) {
						//subRL的右边高
						cur->_bf = 0;
						parent->_bf = -1;
					}
				}

				break;
			}
		}
		return true;
	}

	//右旋
	void rotateR(Node* parent) {
		//左子树的根结点
		Node* subL = parent->_left;
		//左子树的右子树的根节点
		Node* subLR = subL->_right;
		//parent的父结点
		Node* grandp = parent->_parent;


		//修改6个链接
		subL->_right = parent;
		parent->_parent = subL;
		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;

		//如果parent为根节点, 需要更新根节点
		if (parent == _root) {
			_root = subL;
			subL->_parent = nullptr;
		}
		else {
			//链接grandp
			if (grandp->_left == parent)
				grandp->_left = subL;
			else
				grandp->_right = subL;
			subL->_parent = grandp;
		}

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

	//左旋
	void rotateL(Node* parent) {
		//右子树的根节点
		Node* subR = parent->_right;
		//右子树的左子树的根节点
		Node* subRL = subR->_left;
		//parent的父节点
		Node* grandp = parent->_parent;

		//修改6个指针链接
		subR->_left = parent;
		parent->_parent = subR;
		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;

		//判断parent是否为根节点
		if (parent == _root) {
			_root = subR;
			subR->_parent = nullptr;
		}
		else {
			//链接grandp
			if (grandp->_left == parent)
				grandp->_left = subR;
			else
				grandp->_right = subR;
			subR->_parent = grandp;
		}

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


AVL树的其他接口

1. 中序遍历

老生常谈了, 直接上代码吧

	//中序遍历
	void inoder() {
		_inorder(_root);
		cout << endl;
	}

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

		_inorder(root->_left);
		cout << root->_data << " ";
		_inorder(root->_right);
	}

2. 判断是否为AVL树

遍历整棵树, 如果每个结点的平衡因子都在[-1, 1]区间, 则是AVL树, 否则不是

为了检查平衡因子更新错误的问题, 在遍历每一个结点的时候先计算并判断当前结点的平衡因子是否正确, 如果不正确, 输出结点数据信息并退出, 正确则继续遍历

	//判断平衡二叉树
	bool isBalance() {
		return _isBalance(_root);
	}

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

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

		//判断平衡因子的值是否正确
		//bf = right - left
		if (root->_bf != rightHeight - leftHeight) {
			//平衡因子更新有问题
			cout << "当前位置: " << root->_data << " bf: " << root->_bf << endl;
			cout << "left: " << leftHeight << " right: "<< rightHeight << endl;
			return false;
		}

		return abs(root->_bf) < 2
			&& _isBalance(root->_left)
			&& _isBalance(root->_right);
	}

	//获取高度
	int getHeight(Node* root) {
		if (root == nullptr)
			return 0;
		int left = getHeight(root->_left);
		int right = getHeight(root->_right);
		return left > right ? left + 1 : right + 1;
	}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

殇&璃

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

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

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

打赏作者

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

抵扣说明:

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

余额充值