【C++】AVL树

AVL树

在这里插入图片描述

一、底层结构

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


二、AVL树的概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。这就是AVL树。有时也叫它平衡二叉搜索树,因为它保持高度平衡,所以有的地方也叫它高度平衡二叉搜索树

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

  1. 它的左右子树都是AVL树
  2. 任何一颗左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
  • 0代表左右高度相等
  • 1代表右子树高1
  • -1代表左子树高1

下面给出一幅图来判断是否为AVL树:

image-20230329152933687

如果一棵二叉搜索树是高度平衡的(相对平衡),它就是AVL树。如果它有n个结点,其高度可保持在O(logN),搜索时间复杂度O(logN)。


三、AVL树节点的定义

这里我们实现的AVL树为KV模型,自然节点的模板参数有两个,并且节点定义为三叉链结构(左孩子,右孩子,父亲),在二叉链表的基础上加了一个指向父结点的指针域,因为在某个子树插入节点后,如果这课子树的高度发生变化,那么子树的平衡因子需要进行调整,可能需要一路向上调整,比较麻烦,所以这个地方就引入了三叉链结构,使得即便于查找孩子结点,又便于查找父结点。接着还需要创建一个变量_bf作为平衡因子(右子树 - 左子树的高度差)。最后写一个构造函数初始化变量即可。

//节点类
template<class K, class V>
struct AVLTreeNode
{
     
     
	//存储的键值对
	pair<K, V> _kv;
	//三叉连结构
	AVLTreeNode<K, V>* _left;//左孩子
	AVLTreeNode<K, V>* _right;//右孩子
	AVLTreeNode<K, V>* _parent;//父亲
	//平衡因子_bf
	int _bf;//右子树 - 左子树的高度差
	//构造函数
	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _bf(0)
	{
     
     }
};

注意: 给每个结点增加平衡因子并不是必须的,只是实现AVL树的一种方式,不引入平衡因子也可以实现AVL树,只不过会麻烦一点。


四、AVL树的基本框架

此部分内容为AVL树的类,主要作用是来完成后续的插入旋转删除……操作:

//AVL树的类
template<class K, class V>
class AVLTree
{
     
     
	typedef AVLTreeNode<K, V> Node;
public:
    // 插入
	bool Insert(const pair<K, V>& kv);
    // 中序遍历
    void InOrder();
    // 验证
    bool IsBalanceTree();
    // 求高度的子树
	int _Height(Node* root);
    // 修改
    bool Modify(const K& key, const V& value);
    // 查找
    bool Find(const K& key);
private:
    // 判读是否平衡的子树
	bool _IsBalanceTree(Node* root);
     // 中序遍历子函数
    void InOrder(Node* root);
private:
	Node* _root;
};

五、AVL树的插入

插入主要分为这几大步骤:

  • 1、一开始为空树,直接new新节点
  • 2、一开始非空树,寻找插入的合适位置
  • 3、找到插入的合适位置后,进行父亲与孩子的双向链接
  • 4、更新新插入的节点祖先的平衡因子
  • 5、针对不合规的平衡因子进行旋转调整

接下来对其进行逐个分析:

  • 一、一开始为空树,直接new新节点:

因为树为空的,所以直接new一个新插入的节点,将其作为根_ root即可,接着更新平衡因子_bf为0,最后返回true。

  • 二、一开始非空树,寻找插入的合适位置

这里和二叉搜索树的寻找合适的插入位置的思想一样,都要遵循以下几步:

  1. 插入的值 > 节点的值,更新到右子树查找
  2. 插入的值 < 节点的值,更新到左子树查找
  3. 插入的值 = 节点的值,数据冗余插入失败,返回false

注意**cur不能为空,当循环结束的时候,就说明已经找到插入的合适位置**,即可进行下一步链接。

  • 三、找到插入的合适位置后,进行父亲与孩子的双向链接

注意这里节点的构成为三叉链,因此最后链接后孩子和父亲是双向链接,具体操作如下:

  1. 插入的值 > 父亲的值,把插入的值链接在父亲的右边
  2. 插入的值 < 父亲的值,把插入的值链接在父亲的左边
  3. 因为是三叉链,插入后记得**双向链接**(孩子链接父亲)

走到这,说明节点已经插入完毕,但是接下来就要更新平衡因子了

  • 四、更新新插入的节点祖先的平衡因子

当我们插入新节点后,子树的高度可能会发生变化,针对这一变化,我们给出以下要求:

  1. 子树的高度变了,就要继续往上更新(最坏更新到根节点)
  2. 子树的高度不变,则更新完成
  3. 子树违反平衡规则(平衡因子的绝对值 >= 2),则停止更新,需要旋转子树进行调整

具体的更新规则如下:

  1. 新增结点在parent的右边,parent的平衡因子++
  2. 新增结点在parent的左边,parent的平衡因子 –

每更新完一个节点的平衡因子后,都要进行如下判断:

  1. 如果parent的平衡因子等于-1或者1(说明原先是0,插入节点后使左子树或右子树增高了),表明还需要继续往上更新平衡因子。
  2. 如果parent的平衡因子等于0(说明原先是1或-1,插入节点后增加在矮的那一方),表明无需更新平衡因子了。
  3. 如果parent的平衡因子等于-2或者2(说明原先是1或-1,插入节点后增加在高的那一方),表明此时以parent结点为根结点的子树已经不平衡了,需要进行旋转处理。

如图所示:

image-20230330200032428

  • 五、针对不合规的平衡因子进行旋转调整

当parent的平衡因子为-2/2时,cur的平衡因子必定是-1/1而不会是0。

理由如下:

若cur的平衡因子是0,那么cur一定是新增结点,而不是上一次更新平衡因子时的parent,否则在上一次更新平衡因子时,会因为parent的平衡因子为0而停止继续往上更新

而cur是新增结点的话,其父结点的平衡因子更新后一定是-1/0/1,而不可能是-2/2,因为新增结点最终会插入到一个空树当中,在新增结点插入前,其父结点的状态有以下两种可能:

其父结点是一个左右子树均为空的叶子结点,其平衡因子是0,新增结点插入后其平衡因子更新为-1/1。

其父结点是一个左子树或右子树为空的结点,其平衡因子是-1/1,新增结点插入到其父结点的空子树当中,使得其父结点左右子树当中较矮的一棵子树增高了,新增结点后其平衡因子更新为0。

综上所述,当parent的平衡因子为-2/2时,cur的平衡因子必定是-1/1而不会是0。

而又要分为以下4类进行旋转:

  1. 当parent的平衡因子为2,cur的平衡因子为1时,进行左单旋。
  2. 当parent的平衡因子为-2,cur的平衡因子为-1时,进行右单旋
  3. 当parent的平衡因子为-2,cur的平衡因子为1时,进行左右双旋。
  4. 当parent的平衡因子为2,cur的平衡因子为-1时,进行右左双旋。

注意:并且,在进行旋转处理后就无需继续往上更新平衡因子了,因为旋转后树的高度变为插入之前了,即树的高度没有发生变化,也就不会影响其父结点的平衡因子了。具体原因请看后面的旋转讲解。

代码如下:

//Insert插入
bool Insert(const pair<K, V>& kv)
{
     
     
	//1、一开始为空树,直接new新节点
	if (_root == nullptr)
	{
     
     
		//如果_root一开始为空树,直接new一个kv的节点,更新_root和_bf
		_root = new Node(kv);
		_root->_bf = 0;
		return true;
	}
    
	//2、寻找插入的合适位置
	Node* cur = _root;//记录插入的位置
	Node* parent = nullptr;//保存parent为cur的父亲
	while (cur)
	{
     
     
		if (cur->_kv.first < kv.first)
		{
     
     
			//插入的值 > 节点的值
			parent = cur;
			cur = cur->_right;//更新到右子树查找
		}
		else if (cur->_kv.first > kv.first)
		{
     
     
			//插入的值 < 节点的值
			parent = cur;
			cur = cur->_left;//更新到左子树查找
		}
		else
		{
     
     
			//插入的值 = 节点的值,数据冗余插入失败,返回false
			return false;
		}
	}
    
	//3、找到了插入的位置,进行父亲与插入节点的链接
	cur = new Node(kv);
	if (parent->_kv.first < kv.first)
	{
     
     
		//插入的值 > 父亲的值,链接在父亲的右边
		parent->_right = cur;
	}
	else
	{
     
     
		//插入的值 < 父亲的值,链接在父亲的左边
		parent->_left = cur;
	}
	//因为是三叉连,插入后记得双向链接(孩子链接父亲)
	cur->_parent = parent;
    
	//4、更新新插入节点的祖先的平衡因子
	while (parent)//最远要更新到根
	{
     
     
		if (cur == parent->_right)
		{
     
     
			parent->_bf++;//新增结点在parent的右边,parent的平衡因子++
		}
		else
		{
     
     
			parent->_bf--;//新增结点在parent的左边,parent的平衡因子 --
		}
		//判断是否继续更新?
		if (parent->_bf == 0)// 1 or -1 -> 0 填上了矮的那一方
		{
     
     
			//1 or -1 -> 0 填上了矮的那一方,此时正好,无需更新
			break;
		}
		else if (parent->_bf == 1 || parent->_bf == -1)
		{
     
     
			//0 -> 1或-1  此时说明插入节点导致一边变高了,继续更新祖先
			cur = cur->_parent;
			parent = parent->_parent;
		}
		else if (parent->_bf == 2 || parent->_bf == -2)
		{
     
     
			//1或-1 -> 2或-2 插入节点导致本来高的一边又变更高了
			//此时子树不平衡,需要进行旋转
			if (parent->_bf == 2 && cur->_bf == 1)
			{
     
     
				RotateL(parent);//右边高,左单旋
			}
			else if (parent->_bf == -2 && cur->_bf == -1)
			{
     
     
				RotateR(parent);//左边高,右单旋
			}
			else if (parent->_bf == -2 && cur->_bf == 1)
			{
     
     
				RotateLR(parent);//左右双旋
			}
			else if (parent->_bf == 2 && cur->_bf == -1)
			{
     
     
				RotateRL(parent);//右左双旋
			}
			break;
		}
		else
		{
     
     
			//插入之前AVL树就存在不平衡树,|平衡因子| >= 2的节点
			//实际上根据前面的判断不可能走到这一步,不过这里其实是为了检测先前的插入是否存在问题
			assert(false);
		}
	}
	return true;
}

六、AVL树的旋转

AVL树的旋转分为4种:

  1. 左单旋
  2. 右单旋
  3. 左右双旋
  4. 右左双旋

AVL树的旋转要遵循下面四个原则:

  • 1、让这棵子树左右高度差不超过1
  • 2、旋转过程中继续保持它是搜索树
  • 3、更新调整孩子节点的平衡因子
  • 4、让这颗子树的高度跟插入前保持一致

1.左单旋

  • 条件:新节点插入较高右子树的右侧—>右右:左单旋

图示:

image-20230330004009136

鉴于左单旋的情况非常多,这里我们画一张抽象图来演示:

image-20230330105204442

这里的长方形条(a、b、c)表示的是子树,h为子树的高度,而30和60为实打实的节点。

注意:这里表示的是抽象图

image-20230331203212872

左单旋操作步骤

  1. 让subRL变成parent的右子树,更新subRL的父亲为parent

  2. 让subR变成根节点

  3. 让parent变成subR的左子树,更新parent的父亲为subR

  4. 更新平衡因子

左单旋后满足二叉搜索树的性质

  1. subR的左子树当中结点的值本身就比parent的值大,因此可以作为parent的右子树。
  2. parent及其左子树当中结点的值本身就比subR的值小,因此可以作为subR的左子树。

注意:

  1. parent可能为整棵树的一个子树,则需要链接parent的父亲和subR
  2. subRL可能为空,但是更新subRL的父亲为parent是建立在subRL不为空的前提下完成的。所以我们需要做一个判断。

解释为何上述左单旋的可行性:

  • 首先,根据底层二叉搜索树的结构:b节点的值肯定是在30~60之间的,b去做30的右子树没有任何问题,且这里把60挪到根部,随即把30作为60的右子树,这样整体的变化就像是一个左旋一样,即旋转后原来的根节点和现在的根节点的平衡因子都变成0,而且插入前后子树的高度都是h+1,也满足二叉搜索树的性质且均平衡。

代码如下:

void RotateL(Node* parent)
{
     
     
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	Node* ppNode = parent->_parent;//提前保持parent的父亲
	//1、建立parent和subRL之间的关系
	parent->_right = subRL;
	if (subRL)//防止subRL为空
	{
     
     
		subRL->_parent = parent;
	}
	//2、建立subR和parent之间的关系
	subR->_left = parent;
	parent->_parent = subR;
	//3、建立ppNode和subR之间的关系
	if (parent == _root)
	{
     
     
		_root = subR;
		_root->_parent = nullptr;
	}
	else
	{
     
     
		if (parent == ppNode->_left)
		{
     
     
			ppNode->_left = subR;
		}
		else
		{
     
     
			ppNode->_right = subR;
		}
		subR->_parent = ppNode;//三叉链双向链接关系
	}
	//4、更新平衡因子
	subR->_bf = parent->_bf = 0;
}

2.右单旋

  • 条件:新节点插入较高左子树的左侧—>左左:右单旋

图示:

image-20230330114839863

同样这里的长方形条(a、b、c)表示的是子树,h为子树的高度,而30和60为实打实的节点。

上图在插入前,AVL树是平衡的,新节点插入到30的左子树(注意:此处不是左孩子)中,30左子树增加了一层,导致以60为根的二叉树不平衡,要让60平衡,只能将60左子树的高度减少一层,右子树增加一层,即将左子树往上提,这样60转下来,因为60比30大,只能将其放在30的右子树,而如果30有右子树,右子树根的值一定大于30,小于60,只能将其放在60的左子树,旋转完成后,更新节点的平衡因子即可。

在旋转过程中,有以下几种情况需要考虑:

  1. 30节点的右孩子可能存在,也可能不存在
  2. 60可能是根节点,也可能是子树
    1、如果是根节点,旋转完成后,要更新根节点
    2、如果是子树,可能是某个节点的左子树,也可能是右子树

左单旋操作步骤

  1. 让subLR变成parent的左子树,更新subLR的父亲为parent
  2. 让subL变成根节点
  3. 让parent变成subL的右子树,更新parent的父亲为subL
  4. 更新平衡因子

右单旋后满足二叉搜索树的性质:

  1. subL的右子树当中结点的值本身就比parent的值小,因此可以作为parent的左子树。
  2. parent及其右子树当中结点的值本身就比subL的值大,因此可以作为subL的右子树。

注意:

  1. parent可能为整棵树的一个子树,则需要链接parent的父亲和subL。
  2. subLR可能为空,但是更新subLR的父亲为parent是建立在subLR不为空的前提下完成的。

代码如下:

//2、右单旋
void RotateR(Node* parent)
{
     
     
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	Node* ppNode = parent->_parent;
	//1、建立parent和subLR之间的关系
	parent->_left = subLR;
	if (subLR)
	{
     
     
		subLR->_parent = parent;
	}
	//2、建立subL和parent之间的关系
	subL->_right = parent;
	parent->_parent = subL;
	//3、建立ppNode和subL的关系
	if (parent == _root)
	{
     
     
		_root = subL;
		_root->_parent = nullptr;
	}
	else
	{
     
     
		if (parent == ppNode->_left)
		{
     
     
			ppNode->_left = subL;
		}
		else
		{
     
     
			ppNode->_right = subL;
		}
		subL->_parent = ppNode;//三叉链双向关系
	}
	//4、更新平衡因子
	subL->_bf = parent->_bf = 0;
}

image-20230331200434937

我们发现左边插入后形成一条直线,使用一次旋转就可以解决问题,但是如果插入是一条折线,单次左旋或者右旋就不能解决问题。所以接下来我们使用双旋,去解决此问题。

3.左右双旋

  • 条件:新节点插入较高左子树的右侧—>左右:先左旋,再右旋

接下来图示解析左右双旋的具体解法。

  • 1、插入新节点:

image-20230330115228580

此类模型既不满足左单旋的条件也不满足右单旋的条件,但是我们可以将其组合起来,即左右双旋(先左单旋,再右单旋)的办法重新建立平衡。接下来执行下一步左单旋:

  • 2、以节点30(subL)为旋转点左单旋:

image-20230330115305705

此时再观察这幅图,这不就是一个完美的右单旋模型,把60的左子树看成一个整体,此时新插入的节点即插入较高左子树的左侧,刚好符合右单旋的性质,接下来即可进行右单旋:

  • 3、以节点90(parent)为旋转点进行右单旋:

image-20230330115248304

此时左右双旋已经完成。

左右双旋的步骤如下

  1. 以subL为旋转点进行左单旋。
  2. 以parent为旋转点进行右单旋。
  3. 更新平衡因子。

左右双旋后满足二叉搜索树的性质

  1. 左右双旋后,实际上就是让subLR的左子树和右子树,分别作为subL和parent的右子树和左子树,再让subL和parent分别作为subLR的左右子树,最后让subLR作为整个子树的根(结合图理解)。

  2. subLR的左子树当中的结点本身就比subL的值大,因此可以作为subL的右子树。

  3. subLR的右子树当中的结点本身就比parent的值小,因此可以作为parent的左子树。

  4. 经过步骤1/2后,subL及其子树当中结点的值都就比subLR的值小,而parent及其子树当中结点的值都就比subLR的值大,因此它们可以分别作为subLR的左右子树。

最后一步为更新平衡因子,但是更新平衡因子又分如下三类:

  • 1、当subLR原始平衡因子是-1时,左右双旋后parent、subL、subLR的平衡因子分别更新为1、0、0。

image-20230330115824563

  • 2、当subLR原始平衡因子是1时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、-1、0。

image-20230330115856087

  • 3、当subLR原始平衡因子是0时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、0、0。

image-20230331211654214

这里可以看出唯有subLR平衡因子为0的情况下在进行左右旋转后,三个节点的平衡因子都要更新为0。

  • 代码如下:
//3、左右双旋
void RotateLR(Node* parent)
{
     
     
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	int bf = subLR->_bf;//提前记录subLR的平衡因子
	//1、以subL为根传入左单旋
	RotateL(subL);
	//2、以parent为根传入右单旋
	RotateR(parent);
	//3、重新更新平衡因子
	if (bf == 0) // subLR左子树新增
	{
     
     
		parent->_bf = 0;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
	else if (bf == 1) // subLR右子树新增
	{
     
     
		parent->_bf = 0;
		subL->_bf = -1;
		subLR->_bf = 0;
	}
	else if (bf == -1) // subLR就是新增
	{
     
     
		parent->_bf = 1;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
	else
	{
     
     
		assert(false);//此时说明旋转前就有问题,检查
	}
}

4.右左双旋

  • 条件:新节点插入较高右子树的左侧—>右左:先右旋,再左旋

接下来图示解析左右双旋的具体解法。

  • 1、插入新节点:

image-20230330115922431

注意这里的新节点插在了较高右子树的左侧,不能用上文的单旋转以及左右旋转,相反而应使用右左旋转(先右单旋,再左单旋)来解决,接下来执行下一步右单旋:

  • 2、以节点90(subR)为旋转点右单旋:

image-20230330115939601

此时再观察这幅图,这部就是一个妥妥的左单旋模型吗,把60的右子树看成一个整体,此时新插入的节点即插入较高右子树的右侧,刚好符合左单旋的性质,接下来即可进行左单旋:

  • 3、以节点30(parent)为旋转点左单旋:

image-20230330120005425

此时右左双旋已经完成。

右左双旋的步骤如下

  1. 以subR为旋转点进行右单旋。

  2. 以parent为旋转点进行左单旋。

  3. 更新平衡因子。

右左双旋后满足二叉搜索树的性质

  1. 右左双旋后,实际上就是让subRL的左子树和右子树,分别作为parent和subR的右子树和左子树,再让parent和subR分别作为subRL的左右子树,最后让subRL作为整个子树的根(结合图理解)。

  2. subRL的左子树当中的结点本身就比parent的值大,因此可以作为parent的右子树。

  3. subRL的右子树当中的结点本身就比subR的值小,因此可以作为subR的左子树。

  4. 经过步骤1/2后,parent及其子树当中结点的值都就比subRL的值小,而subR及其子树当中结点的值都就比subRL的值大,因此它们可以分别作为subRL的左右子树。

最后一步为更新平衡因子,但是更新平衡因子又分如下三类:

  • 1、当subRL原始平衡因子是-1时,右左双旋后parent、subR、subRL的平衡因子分别更新为0、1、0。

image-20230330120127913

  • 2、当subRL原始平衡因子是1时,右左双旋后parent、subR、subRL的平衡因子分别更新为-1、0、0。

image-20230330120140351

  • 3、当subRL原始平衡因子是0时,右左双旋后parent、subR、subRL的平衡因子分别更新为0、0、0。

image-20230331211717889

这里可以看出唯有subRL平衡因子为0的情况下在进行左右旋转后,三个节点的平衡因子都要更新为0。

  • 代码如下:
//4、右左双旋
void RotateRL(Node* parent)
{
     
     
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	int bf = subRL->_bf;//提前记录subLR的平衡因子
	//1、以subL为根传入左单旋
	RotateR(subR);
	//2、以parent为根传入右单旋
	RotateL(parent);
	//3、重新更新平衡因子
	if (bf == 0) // subRL左子树新增
	{
     
     
		parent->_bf = 0;
		subR->_bf = 0;
		subRL->_bf = 0;
	}
	else if (bf == 1) // subRL右子树新增
	{
     
     
		parent->_bf = -1;
		subR->_bf = 0;
		subRL->_bf = 0;
	}
	else if (bf == -1) // subRL就是新增
	{
     
     
		parent->_bf = 0;
		subR->_bf = 1;
		subRL->_bf = 0;
	}
	else
	{
     
     
		assert(false);//此时说明旋转前就有问题,检查
	}
}

七、AVL树的验证

AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,就是看它是否为二叉搜索树,以及是否为一颗平衡树。接下来分别讨论:

  • 1、验证其为二叉搜索树:

这里我们只需要进行中序遍历看看结果是否可得到一个有序的序列,如果可以则证明是二叉搜索树,而中序遍历的实现非常简单,先前的二叉搜索树的实现已然完成过,这里直接给出代码:

//中序遍历的子树
void _InOrder(Node* root)
{
     
     
	if (root == nullptr)
		return;

	_InOrder(root->_left);
	cout << root->_kv.first << " ";
	_InOrder(root->_right);
}
//中序遍历
void InOrder()
{
     
     
	_InOrder(_root);
	cout << endl;
}

检测是否为二叉搜索树的代码实现后,接下来开始验证是否为平衡树。

  • 2、验证其为平衡树:

规则如下:(递归的思想)

  1. 空树也是平衡树,一开始就要判断
  2. 封装一个专门计算高度的函数(递归计算高度)后续用来计算高度差(平衡因子diff)
  3. 如过diff不等于root的平衡因子(root->_bf),或root平衡因子的绝对值超过1,则一定不是AVL树
  4. 继续递归到子树&&右树,直至结束

补充

求高度采用后序遍历,变量步骤如下:

从叶子结点处开始计算每课子树的高度。(每棵子树的高度 = 左右子树中高度的较大值 + 1)
先判断左子树是否是平衡二叉树。
再判断右子树是否是平衡二叉树。
若左右子树均为平衡二叉树,则返回当前子树的高度给上一层,继续判断上一层的子树是否是平衡二叉树,直到判断到根为止。(若判断过程中,某一棵子树不是平衡二叉树,则该树也就不是平衡二叉树了)

image-20230402165742644

代码如下:

//验证一棵树是否为平衡树
bool IsBalanceTree()
{
     
     
	return _IsBalanceTree(_root); //加不加_都可以,不加_就构成重载
}
//判读是否平衡的子树
bool _IsBalanceTree(Node* root)
{
     
     
	//空树也是AVL树
	if (nullptr == root)
		return true;
	//计算root节点的平衡因子diff:即root左右子树的高度差
	int leftHeight = _Height(root->_left);
	int rightHeight = _Height(root->_right);
	int diff = rightHeight - leftHeight;
	//如果计算出的平衡因子与root的平衡因子不相等,或root平衡因子的绝对值超过1,则一定不是AVL树
	if ((abs(diff) > 1))
	{
     
     
		cout << root->_kv.first << "节点平衡因子异常" << endl;
		return false;
	}
	if (diff != root->_bf)
	{
     
     
		cout << root->_kv.first << "节点平衡因子与root的平衡因子不等,不符合实际" << endl;
		return false;
	}
	//继续递归检测,直到结束
	return _IsBalanceTree(root->_left) 
        && _IsBalanceTree(root->_right);
}
//求高度的子树
int _Height(Node* root)
{
     
     
	if (root == nullptr)
		return 0;
	int lh = _Height(root->_left);
	int rh = _Height(root->_right);
	return lh > rh ? lh + 1 : rh + 1;
}

综合上述两大步骤的操作即可对一棵树充分的验证是否为AVL树。


八、AVL树的修改

实现修改AVL树当中指定key值结点的value,我们可以实现一个Modify函数,该函数当中的逻辑如下:

调用查找函数获取指定key值的结点,对该结点的value进行修改。

代码如下:

//修改函数
bool Modify(const K& key, const V& value)
{
     
     
	Node* ret = Find(key);
	if (ret == nullptr) //未找到指定key值的结点
	{
     
     
		return false;
	}
	ret->_kv.second = value; //修改结点的value
	return true;
}

九、AVL树的查找

Find查找函数的思路很简单,定义cur指针从根部开始按如下规则遍历:

  1. 若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
  2. 若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
  3. 若key值等于当前结点的值,则查找成功,返回true。
  4. 若遍历一圈cur走到nullptr了说明没有此结点,返回false。
//Find查找
bool Find(const K& key)
{
     
     
	Node* cur = _root;
	while (cur)
	{
     
     
		if (cur->_key < key)
		{
     
     
			cur = cur->_right;//若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
		}
		else if (cur->_key > key)
		{
     
     
			cur = cur->_left;//若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
		}
		else
		{
     
     
			return true;//若key值等于当前结点的值,则查找成功,返回true。
		}
	}
	return false;//遍历一圈没找到返回false
}

十、AVL树的删除(了解)

因为AVL树也是二叉搜索树,主要是三个大思路:

  1. 按二叉搜索树的规则删除
  2. 更新平衡因子
  3. 出现不平衡,需要旋转调整

只不过与搜索二叉树的删除不同的是,删除节点后的平衡因子需要不断更新,最差情况下一直要调整到根节点的位置。具体这里就不再实现了,因为着实有点复杂。不过《算法导论》或《数据结构-用面向对象方法与C++描述》殷人昆版这两本书上是有详细的讲解的哈。


十一、AVL树的性能

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

参考博客:AVL树(动图详解)_2021dragon的博客-CSDN博客

猜你喜欢

转载自blog.csdn.net/m0_64224788/article/details/129969535