【C++进阶】深度解析AVL树及其简单模拟实现

一,什么是AVL树

之前我们学习了二叉搜索树,但是有时候因为节点插入的问题,可能会退化为单支树,这样会导致查找效率变得底下如顺序表。
因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:
当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度

这种树就是AVL树.

二,AVL树的特性

AVL树满足两个条件:

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

平衡因子=右子树高度-左子树高度

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

三,模拟实现

1. 基本框架

AVL树是一种平衡二叉树,其内部存储的是pair键值对,我们模拟实现的时候直接用pair来存储即可。
我们先来写AVL的节点定义:
每个节点都要有一个平衡因子用来保证其AVL树特性

template<class K,class V>
struct AVLTreeNode {
	typedef AVLTreeNode<K, V> Node;

	AVLTreeNode(const pair<K,V> &kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
		,_kv(kv)
	{}
	
	Node* _left;
	Node* _right;
	Node* _parent;//记录当前节点的父亲
	int _bf;//记录节点的平衡因子
	pair<K, V> _kv;//保存记录的key,val

};

然后我们来写AVL的框架:

template<class K,class V>
class AVLTree {
	typedef AVLTreeNode<K, V> Node;
public:
	//...
private:
	Node* _root = nullptr;

};

2. 插入(不带旋转)

下面我们来实现AVL树的插入:
插入分为两步:

  1. 按照二叉搜索树那样插入节点
  2. 调整平衡因子

插入节点的部分和二叉搜索树的代码一样,主要是修改平衡因子的部分

当插入新节点后,这个新节点的双亲节点的平衡因子会发生变化:
1.新插入的节点cur在其双亲节点parent的左孩子时,parent的平衡因子-1
2. 新插入的节点在parent的右孩子时,parent的平衡因子+1

代码如下:

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 (kv.first > cur->_kv.first) {
				parent = cur;
				cur = cur->_right;
				
			}
			else if (kv.first < cur->_kv.first) {
				parent = cur;
				cur = cur->_left;
				
			}
			else {
				return false;
			}
		}
		//找到了插入位置
		cur = new Node(kv);
		if (kv.first > parent->_kv.first) {
			parent->_right = cur;
		}
		else {
			parent->_left = cur;
		}
		cur->_parent = parent;

		//修改平衡因子
		while (parent) {
			if (cur == parent->_left) {//如果加在左边,则父节点的平衡因子--
				parent->_bf--;
			}
			else {
				parent->_bf++;//右边则++
			}
			//调整平衡因子
			//...

		}
		return true;
}

修改后这里有三种情况:

  1. 如果pParent的平衡因子为0,说明插入之前pParent的平衡因子为正负1,插入后被调整成0,此时满足AVL树的性质,插入成功
  2. 如果pParent的平衡因子为正负1,说明插入前pParent的平衡因子一定为0,插入后被更新成正负1,此时以pParent为根的树的高度增加,需要继续向上更新
  3. 如果pParent的平衡因子为正负2,则pParent的平衡因子违反平衡树的性质,需要对其进行旋转处理

我们先来说前两种情况,第三种我们在旋转中讲解


第一种情况:
修改后parent的平衡因子为0,这里就不用再进行调整了
在这里插入图片描述

第二种情况:
修改后平衡因子为1,则以parent为根的这颗树的高度发生了变化,则要继续向上调整平衡因子
在这里插入图片描述
代码:

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 (kv.first > cur->_kv.first) {
				parent = cur;
				cur = cur->_right;
				
			}
			else if (kv.first < cur->_kv.first) {
				parent = cur;
				cur = cur->_left;
				
			}
			else {
				return false;
			}
		}
		//找到了插入位置
		cur = new Node(kv);
		if (kv.first > parent->_kv.first) {
			parent->_right = cur;
		}
		else {
			parent->_left = cur;
		}
		cur->_parent = parent;

		//修改平衡因子
		while (parent) {
			if (cur == parent->_left) {//如果加在左边,则父节点的平衡因子--
				parent->_bf--;
			}
			else {
				parent->_bf++;//右边则++
			}
			//增加节点后有三种情况,
			if (parent->_bf == 0) {
				break;
			}
			else if (parent->_bf == 1 || parent->_bf == -1) {
				cur = cur->_parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2) {
				//旋转
				
			}
			else {
				assert(false);//说明插入之前就有问题
			}
		}
		return true;

	}

2. AVL树的旋转

上面的两种情况,更新parent的平衡因子后AVL树的特性还保持着,但是第三种情况更新后,双亲的平衡因子为2/-2,破坏了平衡二叉搜索树的特性,所以就要进行以parent为根的树的旋转

AVL树的旋转也分为四种情况

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

下面我们来依次解释:


右单旋

右单旋是新插入的节点在左子树中,使其整棵树右高左低,所以要旋转右子树来降低高度差使这棵树变得平衡

具体过程看下图:
在这里插入图片描述
这里我们来看一下具体当各子树高度的情况:

在这里插入图片描述

下面我们来编写一下右单璇的代码:
首先我们定义两个节点用来标记当前需要旋转的节点。
在这里插入图片描述

对于右单旋,我们找当前parent的左孩子 subL 和该左孩子的右孩子 subLR

然后我们在旋转时还要注意一下:
(1) 30这个节点的右子树可能存在也可能不存在
不存在时我们就不能直接将subL的parent指针直接指向60

void RotateR(Node* parent) {//右单旋
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		
		parent->_left = subLR;
		if (subLR) {//当subLR存在时,subLR的parent才指向parent
			subLR->_parent = parent;
		}
		//...
}

(2) 60这个节点可能是根也可能不为根。
不为根时,我们还需要一个 ppnode 节点来标记parent的双亲节点,用来将新的’根’节点去链接到原来的树上。

void RotateR(Node* parent) {//右单旋
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		
		parent->_left = subLR;
		if (subLR) {//当subLR存在时,subLR的parent才指向parent
			subLR->_parent = parent;
		}
		if (parent == _root) {//如果p是根,则subL更新为根
			_root = subL;
			subL->_parent = nullptr;
		}
		else {//将旋转后的子树的根节点链接到原来的树上
			if (ppnode->_left == parent) {
				ppnode->_left = subL;
			}
			else {
				ppnode->_right = subL;
			}
			subL->_parent = ppnode;
		}
		//...
}

最后一步就是修改平衡因子,旋转后的节点不用再调整,因为旋转后子树的两端高度都相等,达到平衡。

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

	parent->_left = subLR;
	if (subLR) {//当subLR存在时,subLR的parent才指向parent
		subLR->_parent = parent;
	}

	subL->_right = parent;
	Node* ppnode = parent->_parent;//保存p的parent指向
	parent->_parent = subL;

	if (parent == _root) {//如果p是根,则subL更新为根
		_root = subL;
		subL->_parent = nullptr;
	}
	else {//将旋转后的子树的根节点链接到原来的树上
		if (ppnode->_left == parent) {
			ppnode->_left = subL;
		}
		else {
			ppnode->_right = subL;
		}
		subL->_parent = ppnode;
	}
	//更新节点的平衡因子
	parent->_bf = 0;
	subL->_bf = 0;
}

现在来看左单旋

左单旋和右单旋相反,左边高右边低,所以进行左单旋来降低高度差

在这里插入图片描述
这里的情况都和右单旋转相反,我们直接上代码:

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

	parent->_right = subRL;
	if (subRL) {//当subRL存在时,subRL的parent才指向parent
		subRL->_parent = parent;
	}

	subR->_left = parent;
	Node* ppnode = parent->_parent;//保存p的parent指向
	parent->_parent = subR;

	if (parent == _root) {//如果p是根,则subR更新为根
		_root = subR;
		subR->_parent = nullptr;
	}
	else {//将旋转后的子树的根节点链接到原来的树上
		if (ppnode->_left == parent) {
			ppnode->_left = subR;
		}
		else {
			ppnode->_right = subR;
		}
		subR->_parent = ppnode;
	}
	//更新节点的平衡因子
	parent->_bf = 0;
	subR->_bf = 0;
}

我们现在来看左右双旋,先左单旋再右单旋

这里的左右双旋其实就是相当右单旋的那种场景下,将b子树拆分成两颗子树,再将新增节点加在拆分后的子树上
在这里插入图片描述
我们以加在左子树为例讲解:
对新增节点后的树进行旋转,先以subL为根进行左旋,

在这里插入图片描述

再对parent为根进行右旋
在这里插入图片描述

加在右子树的过程和这个相反,希望大家可以自己去推导…

知道了大概的过程,我们现在来写代码:
和单旋一样,我们定义变量来协助我们旋转

void RotateRL(Node* parent) {//双旋(先左单旋,再右单旋)
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		//..
}

这里需要注意:新增节点所加的位置有三种情况
(1) 加在拆分后的左子树上
(2) 加在拆分后的右子树上
(3) 上图的60这个位置本身是空的
在这里插入图片描述
那么我们如何区分这三种情况呢,
我们可以用subLR的平衡因子来区分,subLR的因子为-1时,说明左高,则加在了左子树上,为1时,说明右高,则加在了右子树,为0时说明这个位置原来是空的。

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

	int bf = subLR->_bf;
	RotateL(subL);
	RotateR(parent);

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

右左双旋
右左双旋和左右双旋相反,各位老铁可以参考左右双旋来自己去画出图来理解

在这里插入图片描述

代码:

void RotateRL(Node* parent) {//双旋(先右单旋,再左单旋)
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	int bf = subRL->_bf;//以subRL的因子为标准判断所加的子树的位置
	RotateR(subR);
	RotateL(parent);

	//修改平衡因子
	//增加节点后有三种情况
	subRL->_bf = 0;
	if (bf == -1) {//加在左子树上
		parent->_bf = 0;
		subR->_bf = 1;
	}
	else if (bf == 1) {
		parent->_bf = -1;
		subR->_bf = 0;
	}
	else if (bf == 0) {
		parent->_bf = 0;
		subR->_bf = 0;
	}
	else {
		assert(false);
	}
}

3. AVL树的验证

AVL的插入讲完了,我们来看看如何证明咋们模拟实现的就是AVL树
一棵树如果是AVL树,那么首先它是一个二叉搜索树,其次他的每个子树的高度差不能超过1

这里我们做了一点小优化,我们在判断时先传入高度,判断高度差时类似于后序遍历的方式,从下往上去计算高度差

代码如下:

bool IsBalance() {
	int height = 0;
	return _IsBalance(_root, height);
}


bool _IsBalance(Node* root,int &height) {
	if (root == nullptr) {
		height = 0;
		return true;
	}
	int leftHeight = 0, rightHeight = 0;
	if (!_IsBalance(root->_left, leftHeight) || !_IsBalance(root->_right, rightHeight)) {
		return false;
	}
	if (abs(rightHeight - leftHeight) >= 2) {
		cout <<root->_kv.first<< "不平衡" << endl;
		return false;
	}
	if (rightHeight - leftHeight != root->_bf) {
		cout << root->_kv.first << "平衡因子异常" << endl;
		return false;
	}
	height = leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
	return true;

}

四,总结

我们今天终于讲完了AVL树,我们的C++部分也开始上了难度,希望大家可以跟上我们的讲解,下一节我们要来开始手撕红黑树!!!
在此之前我们要熟悉前面的二叉搜索树和AVL树的内容,希望大家都能学好C++。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值