C++ 高阶数据结构:AVL 树万字详解|原理、四种旋转、key‑value 实现、调试校验、set/map/红黑树 底层前置知识

目录

引言:

AVL树的概念

AVL树的实现

1.AVL树的结构

2.AVL树的插入

2.1.AVL树插入一个值的大概过程

2.2.平衡因子更新

[AVL 平衡因子更新原则](#AVL 平衡因子更新原则)

回溯更新的三种终止情况

三种回溯情况分析

2.3.插入节点及更新平衡因子的代码实现

3.旋转

3.1.AVL旋转的核心原则

3.2.右单旋(LL场景)

3.3.右单旋代码实现

3.4.左单旋(RR场景)

3.5.左单旋代码实现

3.6.左右双旋(LR场景)

3.7.左右双旋代码实现

3.8.右左双旋(RL场景)

3.9.右左双旋代码实现

4.AVL树的查找

5.AVL树的平衡检测

6.AVL树的删除

删除完整流程简述

结语:


引言:

前面我们完整学习了二叉搜索树(BST),二叉搜索树查询、插入、删除理想情况下时间复杂度为 O(logN)。 但是BST有一个致命缺陷:当输入数据有序或者接近有序时,树会退化成单链,直接降级为链表,查询效率退化到 O(N),完全失去二叉树的优势

为了解决二叉搜索树退化的问题,前辈们提出了多种平衡二叉搜索树。AVL树是最早被发明的平衡二叉搜索树,它通过平衡因子约束,保证任意节点左右子树高度差不超过 1,以此强制把树维持平衡,保证增删查的时间复杂度稳定在 O(logN)。

AVL树也是学习红黑树的重要前置基础,搞懂AVL的旋转逻辑,后续再学习红黑树会轻松很多。 本文将从原理出发,完整实现一套 key‑value 版本、不允许键冗余的AVL树,包含:AVL概念、插入、四大旋转(LL/RR/LR/RL)、查找、平衡校验;同时剖析AVL删除的核心逻辑与面试考点。 >提示:阅读本文前,需要掌握二叉搜索树BST

前置知识(二叉搜索树(BST树))------>

C++ 高阶数据结构:二叉搜索树(BST)万字详解 | 原理、增删查、key 与 key‑value 双版本实现、看懂 set/map 底层-CSDN博客

那么话不多说,接下来进入正文------------------------------>

​​​​​​​ ​​​​​​​


AVL树的概念

  • AVL 树是最先发明的自平衡二叉搜索树,AVL 是一颗空树,或者具备下列性质的二叉搜索树:它的左右子树都是 AVL 树,且左右子树的高度差的绝对值不超过 1。AVL 树是一颗高度平衡搜索二叉树,通过控制高度差去控制平衡。(任何节点的左右子树高度差都为1)

  • AVL 树得名于它的发明者 G. M. Adelson‑Velsky 和 E. M. Landis 是两个前苏联的科学家,他们在 1962 年的论文《An algorithm for the organization of information》中发表了它。

  • AVL 树实现这里我们引入一个平衡因子 (balance factor) 的概念,每个结点都有一个平衡因子,结点平衡因子 = 右子树高度 − 左子树高度 ,也就是说任何结点的平衡因子等于 0、1、-1

    平衡因子并不是 AVL 树的必需成员,但它可以方便我们观察、判断树是否平衡,相当于平衡状态的风向标。

  • 思考一下为什么 AVL 树是高度平衡搜索二叉树,要求高度差不超过 1,而不是高度差是 0 呢?0 不是更好的平衡吗?

    不是不想这样设计,而是有些情况是做不到高度差是 0 的。比如一棵树是 2 个结点,4 个结点等情况下,高度差最好就是 1,无法做到高度差是 0,要做到高度差是0这棵树就是满二叉树了

  • AVL 树整体结点数量和分布和完全二叉树类似,高度可以控制在log N,那么增删查改的效率也可以控制在O(\log N),相比二叉搜索树有了本质的提升(二叉搜索树最坏情况会变成O(N)的时间复杂度)。

举个例子,下图就是一个AVL树

当我们插入一个13之后,树就会变成下图的样子

这个时候,这个树就不是AVL树了,因为10的节点平衡因子高了,这个时候我们就需要将这个二叉搜索树进行旋转,再次变为AVL树,如下图

具体怎么旋我会在下面底层实现的时候具体讲


AVL树的实现

我们这里实现的是key_value模式不支持冗余的AVL树

1.AVL树的结构

AVL树的基础还是二叉搜索树(所以它还是分俩种模式------key模式,key_value模式,各模式依旧支持俩种形态------支持冗余和不支持冗余),所以AVL树的结构没有什么太大的变化,因为要涉及旋转和平衡因子,所以新增俩个成员变量------_parent(指向父亲节点),_bf(平衡因子)

如下

cpp 复制代码
#pragma once
#include<iostream>

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

	AVLTreeNode() = default;
	
	AVLTreeNode(const std::pair<K, V>& kv)
		:_kv(kv)
	{

	}

	std::pair<K, V> _kv;
	Node* _left = nullptr;
	Node* _right = nullptr;
	Node* _parent = nullptr;
	int _bf = 0;
};

template<class K,class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:

private:
	Node* root = nullptr;
};

2.AVL树的插入

2.1.AVL树插入一个值的大概过程

  1. BST 插入:新结点严格按照二叉搜索树规则完成插入。
  2. 回溯更新平衡因子 :新增叶子结点只会影响其祖先的高度。我们从新结点出发向上走到根,沿路更新祖先结点的平衡因子。最坏需要更新至根节点;部分条件下可以提前终止回溯。
  3. 无失衡 :沿路所有结点平衡因子都处于合法区间[-1, 1],插入流程结束。
  4. 发生失衡 :遇到平衡因子超标的结点,执行对应旋转操作(一旦做了旋转,不需要继续往上更新平衡因子,旋转后的子树高度和插入之前一致,上层祖先不受影响)。旋转修复平衡的同时会降低当前子树的高度,不再对更上层结点造成影响,插入流程结束。

2.2.平衡因子更新

AVL 平衡因子更新原则

平衡因子定义:bf = 右子树高度 - 左子树高度

  1. 只有子树高度改变,结点平衡因子才会改变。
  2. 新结点插入右子树:父结点bf++;插入左子树:父结点bf--
  3. 是否继续向上回溯,看当前子树总高度是否变化

回溯更新的三种终止情况

从新插入结点向上走到根,沿路修正祖先平衡因子,分 3 种情况:

  • 更新后 bf = 0 子树高度没有变化,上层不受影响停止回溯

  • 更新后 bf = 1 或 bf = -1 子树平衡,但整体高度 + 1,会影响上层祖先继续向上更新

  • 更新后 bf = 2 或 bf = -2,结点失衡 触发LL/RR/LR/RL 旋转旋转会修复平衡,同时把子树高度恢复到插入前上层不受影响旋转完成直接结束,不再向上走

注:如果一直没有遇到 0、没有失衡,走到根结点,根结点 bf 为 ±1 也停止回溯。

特别注意:只有插入会让子树高度增加;删除结点逻辑是反过来的,平衡因子增减逻辑、停止条件不完全一样,上面这套只适用于插入流程

三种回溯情况分析

回溯到中途需要旋转处理

13插入后,14的平衡因子--(是14的左孩子),随后14的平衡因子变为-1了,继续往上回溯 ,10的平衡因子++(是10的右孩子),此时10节点平衡因子为 2了,不平衡了,走旋转,结束回溯

回溯到中途停止回溯

1节点的平衡节点--,变为-1,接着向上回溯,3的平衡因子--,变为0,说明3节点的左右子树此时相同高度了,没有增高,就不用再回溯了,停止回溯

回溯到根节点

6节点平衡因子++,变为1,接着向上回溯,3节点++,变为1,接着向上回溯,8节点--,变为-1,属于平衡范畴,8为根节点,结束回溯

2.3.插入节点及更新平衡因子的代码实现

插入节点就是BST树的插入方式,更新平衡因子在代码层面是十分简单的,我们需要了解清楚原理

这里直接放源码了

cpp 复制代码
#pragma once
#include<iostream>
#include<utility>//pair头文件
#include<cassert>

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

	AVLTreeNode() = default;
	
	AVLTreeNode(const std::pair<K, V>& kv)
		:_kv(kv)
	{

	}

	std::pair<K, V> _kv;
	Node* _left = nullptr;
	Node* _right = nullptr;
	Node* _parent = nullptr;
	int _bf = 0;
};

template<class K,class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	bool insert(const std::pair<K, V>& kv);
private:
	Node* _root = nullptr;
};

template<class K,class V>
bool AVLTree<K, V>::insert(const std::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);
	cur->_parent = parent;
	if (kv.first < parent->_kv.first)
		parent->_left = cur;
	else
		parent->_right = cur;


	while (parent)
	{
		if (parent->_left == cur)
			parent->_bf--;

		else
			parent->_bf++;
		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)
		{
			//旋转
			break;
		}
		else
		{
			assert(false);
		}
	}
	return true;
}

这边新增assert(false)可以在我们实现旋转的时候方便看出旋转后平衡因子什么有没有出问题,调试时候能用,Release版本下这断言就没用了


3.旋转

3.1.AVL旋转的核心原则

旋转操作要满足两个核心目标:

  1. 维持二叉搜索树特性:旋转前后,树的中序遍历序列保持不变。
  2. 修复失衡并降低子树高度 :把失衡的子树调整为平衡;同时让该子树高度恢复到插入之前,上层结点不再受本次插入的影响。(前面回溯更新平衡因子时提到旋转完成之后不需要继续向上回溯,根源就是旋转后子树高度复原,上层祖先的平衡因子不受影响。)
    AVL 一共有 4 种旋转:
  • 单旋:左单旋(RR)、右单旋(LL)------原理一样
  • 双旋:左右双旋(LR)、右左双旋(RL)

注:我下面讲解的图示里的结点数值仅做演示,业务中只要结点大小满足 BST 规则即可,和具体数字无关。

3.2.右单旋(LL场景)

上图代表了所有右单旋的场景

上图以结点 10 作为失衡根结点,a、b、c代表三棵高度为h(h≥0)的合法 AVL 子树。

注意:10 不一定是整棵树的根,也可能是大树内部的局部子树根。a/b/c 是抽象模型,覆盖全部 LL 右单旋场景,多种具象形态我会在后续讲一讲

  1. 失衡产生过程

    向左侧 a 子树插入新节点,a 子树高度从h变成h+1。向上更新平衡因子后,结点 10 的平衡因子由‑1变为‑2,左子树过高,AVL 平衡被打破,触发右单旋。

  2. 右单旋核心操作

    依据 BST 大小关系:5 < b子树所有结点 < 10

  3. 将 b 子树挂载为结点 10 的左子树;

  4. 将结点 10 挂载为结点 5 的右子树;

  5. 结点 5 成为当前子树新的根。

:上述的3,4,5步骤不是具体实现的流程操作,这里只是讲思路

旋转完成后:

  • 维持二叉搜索树性质;
  • 子树高度回到插入之前的h+2
  • 因为子树高度没有变化,上层祖先平衡因子不受干扰,不需要继续向上回溯,插入结束

接下来我们来看看具体的样例来辅助理解

这是子树高度为0的情况 ------10在我们插入数据后失衡了,这个时候进行右旋,5作为根节点,10的left指向b子树(nullptr),5的right指向10节点

这是子树高度为1的情况------此时abc子树深度均为1,我们插入一个新节点后,以10为根的子树失衡了,此时我们要右旋,和上面一样,5的right指向10节点,10的left指向b子树的根节点(也就是次失衡点的right),然后根节点从10变为了5

这是子树高度为2的情况------这种情况下,b,c可以是x,y,z的任意情况,因为构成AVL树,但是a子树必须是x情况才可以,因为若x是y或z的子树情况下,这个时候我们若插入的最终结果就有俩种情况,插入最后定位在深一点的节点和插入最后定位在浅一点的节点,如果在深一点的节点,那平衡因子更新就更新不到上面,因为底下就已经平衡因子失衡了。如果在浅一点的节点,那就连失衡都没有失衡了,如下面俩张附图,翻转自然就是10节点的left指向5节点的right,5节点的right指向10节点,5作为根节点

附图

这是子树高度为3的情况------从高度为3开始就会比较复杂了,b,c只要是y-C的AVL树就可以了,AVL树的深度只要是4就可以了,然后a的要求就比较高了,a有俩种情况,一种是深度为4满二叉树的AVL树,另一种是深度为4,但深度为4部分的节点为3个的AVL树,如附图。其他情况都是不行的,这里就不推了,原理和深度为2大同小异的,我这里附一下a满足的俩种AVL树。翻转就和上面一样翻转方式

附图

这里演示几个实例主要是为了方便你们理解

3.3.右单旋代码实现

右单旋的代码实现方式有很多种,没有固定的定式,只要实现效果就好了(目的就是让次失衡点指向失衡点的父节点,失衡点的父节点指向次失衡点,次失衡点的right连接失衡点,失衡点连接次失衡点的右子树的根节点)、最后更新平衡因子不要忘了

我这里用最容易看懂的方式实现(即涉及到需要更改的指针我们都存上)这种方式的好处就是易看懂且不用太在乎先后顺序

parent:失衡节点

subL:次失衡节点

subLR:次失衡节点的右子树的根节点

grandp:失衡节点的父节点

cpp 复制代码
template<class K,class V>
void AVLTree<K, V>::RotateLL(Node* parent)
{
	Node* grandp = parent->_parent;
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

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

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

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

当然也是可以用双参数或者三参数来实现的,这个时候就需要在意一点代码的逻辑顺序了

我这里演示一下用双参数的方式(这里我用parent和subL)

cpp 复制代码
template<class K,class V>
void AVLTree<K, V>::RotateLL(Node* parent)
{
	Node* subL = parent->_left;
	if (parent->_parent)
	{
		if (parent->_parent->_left == parent)
			parent->_parent->_left = subL;
		else
			parent->_parent->_right = subL;
	}
	else
	{
		_root = subL;
	}
	subL->_parent = parent->_parent;
	
	parent->_left = subL->_right;
	if (subL->_right)
		subL->_right->_parent = parent;

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

旋转实现完了之后我们就要更新一下节点的平衡因子了

在单旋场景里,只有失衡点和次失衡点动了,别的点没动,所以别的点的平衡因子是没有变化的,我们只需要改动了的点的平衡因子即可(这个我们通过图就可以观察到,这俩个点的平衡因子都变成0了)

所以我们加上更新平衡因子,这个单旋就实现好了

cpp 复制代码
template<class K, class V>
void AVLTree<K, V>::RotateLL(Node* parent)
{
	Node* grandp = parent->_parent;
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

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

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

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

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

接下来我们就要在先前失衡部分去调用这个右旋函数,如下

cpp 复制代码
		else if (parent->_bf == -2 || parent->_bf == 2)
		{
			//旋转
			if (parent->_bf == -2 && cur->_bf == -1)
				RotateLL(parent);

			break;
		}

3.4.左单旋(RR场景)

其实就跟右单旋是同一个逻辑,就不重复讲了,进这个旋的场景就是失衡点的平衡因子为2,次失衡点的平衡因子为1

3.5.左单旋代码实现

这里就直接赋代码了

cpp 复制代码
		else if (parent->_bf == -2 || parent->_bf == 2)
		{
			//旋转
			if (parent->_bf == -2 && cur->_bf == -1)
				RotateLL(parent);
			else if (parent->_bf == 2 && cur->_bf == 1)
				RotateRR(parent);
			break;
		}


template<class K,class V>
void AVLTree<K, V>::RotateRR(Node* parent)
{
	Node* grandp = parent->_parent;
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

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

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

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

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

3.6.左右双旋(LR场景)

LL 右单旋可以解决:失衡点 bf = -2,左孩子 bf = -1,新节点插入左孩子的左子树 。 但是还有另一种情况:失衡点平衡因子为-2,它的左孩子平衡因子为1。

也就是:整体看失衡节点左子树更高,但是新增节点插在了左孩子的右子树,这就是 LR(左‑右)类型失衡。

如果我们直接调用右单旋,无法修复平衡。 我们看两组实例:

  • 情况 1:插入前 a/b/c 子树高度 h = 0
  • 情况 2:插入前 a/b/c 子树高度 h = 1

情况一

情况二

可以看到,单纯一次右单旋操作之后,树依旧是不平衡的。 原因:站在失衡点 10 视角,左子树更高;但是站在它左孩子 5 的视角,它自己的右子树更高。属于双向方向的失衡,单旋只能处理单一方向的失衡。

LR 的标准修复流程:左右双旋

  1. 第一步:对次失衡点(5)执行左单旋,把树结构转换成标准 LL 形态;
  2. 第二步:对原失衡点(10)执行右单旋,完成平衡修复。

我对上面俩种情况的正确旋转流程图演示一下

插入前h为0时的情况

插入前h为1时的情况

LR 双旋完成之后,5、8、10 三个节点的平衡因子并不是固定全部等于 0! 最终的平衡因子取值,取决于新节点到底插入 b 子树内部哪个位置。

我们把 b 子树的根节点 8 拆开,向下拆出 e(左子树)、f(右子树)。原来 a/b/c 子树高度为 h,e、f 初始高度为 h‑1。一共分出 3 种场景。

  • 场景 1:h >= 1,新节点插入 e 子树(8 的左子树) e 子树高度 h‑1 增长到 h,向上更新 8→5→10 平衡因子,触发失衡。 旋转前:节点 8 的平衡因子 = -1 双旋结束:8->_bf=05->_bf=010->_bf=1

  • 场景 2:h >= 1,新节点插入 f 子树(8 的右子树) f 子树高度 h‑1 增长到 h,向上更新 8→5→10 平衡因子,触发失衡。 旋转前:节点 8 的平衡因子 = 1 双旋结束:8->_bf=010->_bf=05->_bf=-1

  • 场景 3:h == 0,a/b/c 全部为空,b 节点本身就是新增节点 旋转前:节点 8 的平衡因子 = 0 双旋结束:5、8、10三者平衡因子全部置0

对应对称的 RL(右‑左双旋)逻辑完全镜像,同样存在 3 套平衡因子结果

3.7.左右双旋代码实现

实现起来就很简单了,只需要照着推理出来的图,把平衡因子的更新情况更新好即可

cpp 复制代码
		else if (parent->_bf == -2 || parent->_bf == 2)
		{
			//旋转
			if (parent->_bf == -2 && cur->_bf == -1)
				RotateLL(parent);
			else if (parent->_bf == 2 && cur->_bf == 1)
				RotateRR(parent);
			else if (parent->_bf == -2 && cur->_bf == 1)
				RotateLR(parent);

			break;
		}


template<class K, class V>
void AVLTree<K, V>::RotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	int bf = subLR->_bf;
	RotateRR(subL);
	RotateLL(parent);
	if (bf == 0)
	{
		subLR->_bf = subL->_bf = parent->_bf = 0;
	}
	else if (bf == 1)
	{
		subLR->_bf = parent->_bf = 0;
		subL->_bf = -1;
	}
	else if (bf == -1)
	{
		subLR->_bf = subL->_bf = 0;
		parent->_bf = 1;
	}
	else
	{
		assert(false);
	}
}

3.8.右左双旋(RL场景)

右左双旋 RL,是左右双旋 LR 的镜像情况:父节点失衡,右子树的左子树插入造成失衡,先对右子树做 LL 左单旋,再对父节点做 RR 右单旋

下面我们将 a/b/c 子树抽象为高度 h 的 AVL 子树进行分析,另外我们需要把 b 子树的细节进一步展开为 subRL 和它高度 h‑1 的 e、f 子树,因为我们要对 b 的父亲 subR 做旋转,左单旋需要处理 b 子树的左子树。b 子树中新增结点的位置不同,平衡因子更新细节不同,观察 subRL 的平衡因子,分三个场景讨论。

  • 场景 1:h>=1,新增结点插入 e 子树,e 子树高度 h‑1→h,向上更新 subRL→subR→parent 平衡因子触发旋转;subRL->_bf == -1。旋转后:subRL=0,parent=0,subR=1
  • 场景 2:h>=1,新增结点插入 f 子树,f 子树高度 h‑1→h,向上更新 subRL→subR→parent 平衡因子触发旋转;subRL->_bf == 1。旋转后:subRL=0,subR=0,parent=-1
  • 场景 3:h==0,a/b/c 全空,subRL 本身就是新增节点;subRL->_bf ==0。旋转后三者subRL、subR、parent全部置 0。

3.9.右左双旋代码实现

cpp 复制代码
		else if (parent->_bf == -2 || parent->_bf == 2)
		{
			//旋转
			if (parent->_bf == -2 && cur->_bf == -1)
				RotateLL(parent);
			else if (parent->_bf == 2 && cur->_bf == 1)
				RotateRR(parent);
			else if (parent->_bf == -2 && cur->_bf == 1)
				RotateLR(parent);
			else if (parent->_bf == 2 && cur->_bf == -1)
				RotateRL(parent);
			break;
		}



template<class K, class V>
void AVLTree<K, V>::RotateRL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	int bf = subRL->_bf;
	RotateLL(subR);
	RotateRR(parent);
	if (bf == 0)
	{
		subRL->_bf = subR->_bf = parent->_bf = 0;
	}
	else if (bf == 1)
	{
		subRL->_bf = subR->_bf = 0;
		parent->_bf = -1;
	}
	else if (bf == -1)
	{
		subRL->_bf = parent->_bf = 0;
		subR->_bf = 1;
	}
	else
	{
		assert(false);
	}
}

4.AVL树的查找

用二叉搜索树(BST)的逻辑实现即可,效率为O(logN)

cpp 复制代码
template<class K,class V>
typename AVLTree<K, V>::Node* AVLTree<K, V>::find(const K& key)
{
	Node* cur = _root;
	while (cur)
	{
		if (cur->_kv.first < key)
			cur = cur->_right;
		else if (cur->_kv.first > key)
			cur = cur->_left;
		else
			return cur;
	}
	return nullptr;
}

5.AVL树的平衡检测

我们实现了这个AVL树后,我们得验证这个AVL树功能有没有问题,这个时候我们不能通过平衡因子去检测是否是AVL树(平衡因子可能也会出问题),我们要通过左子树和右子树的深度差来进行判断

检测平衡函数(在意效率的话可以用尾递归加记忆化搜索来提效,我这里就直接用最简单的递归方式来实现了)

cpp 复制代码
template<class K,class V>
int AVLTree<K, V>::_height(Node* root)
{
	if (nullptr == root)
		return 0;
	int leftheight = _height(root->_left);
	int rightheight = _height(root->_right);

	return leftheight > rightheight ? leftheight + 1 : rightheight + 1;
}

template<class K,class V>
bool AVLTree<K, V>::isAVLTree()
{
	return _isAVLTree(_root);
}


template<class K, class V>
bool AVLTree<K, V>::_isAVLTree(Node* root)
{
	if (nullptr == root)
		return true;

	int leftheight = _height(root->_left);
	int rightheight = _height(root->_right);
	int bf = rightheight - leftheight;
	if (abs(bf) >= 2)
	{
		std::cout <<root->_kv.first<< "高度差异常" << std::endl;
		return false;;
	}
	if (bf != root->_bf)
	{
		std::cout << root->_kv.first << "平衡因子异常" << std::endl;
		return  false;
	}

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

测试代码

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include "AVL Tree.h"
#include <string>
#include <vector>
#include <cstdlib>
#include <ctime>
using namespace std;

// 测试代码 
void TestAVLTree1()
{
    AVLTree<int, int> t;
    // 常规的测试用例 
    //int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
    // 特殊的带有双旋场景的测试用例 
    int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
    for (auto e : a)
    {
        t.insert({ e, e });
    }
    cout << t.isAVLTree() << endl;
}

// 插入一堆随机值,测试平衡,顺便测试一下高度和性能等 
void TestAVLTree2()
{
    const int N = 100000;
    vector<int> v;
    v.reserve(N);
    srand((unsigned int)time(0));
    for (size_t i = 0; i < N; i++)
    {
        v.push_back(rand() + i);
    }
    size_t begin2 = clock();
    AVLTree<int, int> t;
    for (auto e : v)
    {
        t.insert(make_pair(e, e));
    }
    size_t end2 = clock();
    cout << "Insert:" << end2 - begin2 << endl;
    cout << t.isAVLTree() << endl;

    size_t begin1 = clock();
    // 确定在的值 
    /*for (auto e : v)
    {
        t.Find(e);
    }*/
    // 随机值 
    for (size_t i = 0; i < N; i++)
    {
        t.find((rand() + i));
    }
    size_t end1 = clock();
    cout << "Find:" << end1 - begin1 << endl;
}



int main()
{
    TestAVLTree1();
    //TestAVLTree2();
    return 0;
}

6.AVL树的删除

AVL树的删除相比插入就比较复杂,简单的说就是会涉及回溯,删除后平衡因子变为0继续向上更新,变为1或-1停止更新,变为2或-2就旋转,旋转完再根据情况是否向上继续更新

插入 :新节点只会让一条路径高度 + 1;一旦旋转,子树高度复原,直接 break 停止回溯删除 :删除节点会让某棵子树高度 - 1;就算做了旋转,上层祖先依然有可能继续失衡,不能 break,必须继续向上回溯

删除完整流程简述

  1. BST 标准删除(三种情况:叶子、单孩子、双孩子找后继替换)
  2. delCur真正被移除,拿到它的父节点parent作为回溯起点;
  3. parent开始向上循环:
    1. 判断:是左子树少了高度,则parent->_bf++;右子树少了高度,则parent->_bf--
    2. parent->_bf == 0:本节点左右等高,本节点高度减 1,继续向上走
    3. parent->_bf == 1 || parent->_bf == -1:本树高度不变,直接 break,终止回溯
    4. parent->_bf == 2 || parent->_bf == -2:发生失衡,执行对应旋转(LL/RR/LR/RL)。
      • 旋转完成不能直接 break!
      • 旋转后的子树根节点的_bf会有三种结果(0 / 1 /-1),决定上层是否继续:
        • 如果旋转后新根 bf == 0:代表这棵子树高度降低,需要继续向上回溯
        • 如果旋转后新根 bf == ±1:代表子树高度不变,可以 break 停止
  4. 循环直到parent == nullptr结束。

插入旋转后直接 break;删除旋转之后要看旋转之后新根的 bf,再决定要不要继续往上,不是固定写死 break 或者继续

不能直接break的原因:

插入新增节点只会让路径高度 + 1;旋转之后子树高度恢复成原来的高度,上层不受影响。 删除会让子树高度减 1;就算局部旋转修复当前节点,上层祖先依然可能再次失衡,必须继续向上。

这里就不讲具体实现了(因为面试基本不会问删除,我们正常使用的时候也不会去手搓数据结构),就是相比插入而言多了个特判


结语:

用C++实现的高阶数据结构的AVL树部分的内容就全部讲解完毕啦,希望以上内容对你有所帮助,感谢观看,若觉得写的还可以,可以分享给朋友一起来看哦,毕竟一起进步更有动力嘛,当然能关注一下就更好啦。

相关推荐
爱和冰阔落1 小时前
【Linux】两个毫无关系的进程怎么通信?命名管道 FIFO 从原理到 Server/Client 实战
android·linux·数据库·c++·vim
疯狂打码的少年2 小时前
【数据结构】顺序表 vs 链表的对比与选择
数据结构·笔记·链表
码匠许师傅9 小时前
【C++ 面试真题】C++ 的 const 和 constexpr 有什么区别?
c++·面试
wabs66612 小时前
关于图论【最短路径之Dijkstra算法(堆优化版)|卡码网47.参加科学大会的思考】
数据结构·算法·图论·优先级队列·邻接表·小顶堆·卡码网
果果燕12 小时前
实习笔记(一):NFS、Samba、VNC、Git、CMake、systemd、内核、交叉编译
linux·arm开发·c++·笔记·git
牛艺翔12 小时前
C++基础
开发语言·c++
键盘会跳舞12 小时前
C++ :容器适配器stack源码级拆解
开发语言·c++··stack·先进后出
不负岁月无痕13 小时前
简单理解操作系统结构
java·linux·c语言·开发语言·c++·面试
余额瞒着我当琳14 小时前
C++模板初阶与STL初探
android·java·c++