数据结构 --- AVL树

数据结构 --- AVL树

一、什么是AVL数

前面学习了二叉搜索树,而AVL树是自平衡二叉搜索树,核心目标是控制左右子树高度的绝对差值不会超过1,控制高度差来控制平衡。

AVL树整体节点数量和分布与完全二叉树类似,高度可以控制在logN,那么增删查改的效率也可以控制在O(logN),相比二叉搜索树又有了本质提升。

二、什么是平衡因子

AVL树控制高度的实现需要引入平衡因子,每个节点都有一个平衡因子,任何节点的平衡因子等于右子树的高度减去左子树的高度,也就是说任何节点的平衡因子等于-1/0/1。

注意:AVL树并不是必须要平衡因子,但有了平衡因子可以更加方便我们控制和观察这颗树是否平衡。

三、AVL树实现

3.1 AVLTreeNode 基本结构

AVL 树的节点在普通二叉搜索树节点的基础上,增加了两个关键成员:父节点指针 _parent平衡因子 _bf

节点结构详解

1. 键值对 _kv

pair<K, V> _kv 存储节点的实际数据,K 为键类型(用于比较和排序),V 为值类型。AVL 树整体上也是一棵搜索树,因此键 K 必须支持比较运算(如 operator<)。

2. 左右孩子指针 _left / _right

AVLTreeNode<K, V>* _left_right 分别指向当前节点的左孩子和右孩子。这是二叉搜索树的常规成员,用于构建树形结构。

3. 父节点指针 _parent

AVLTreeNode<K, V>* _parent 指向当前节点的父亲节点。这一指针并非搜索树的必需成员 ,但在 AVL 树中至关重要------当插入或删除节点后,我们必须自底向上 逐层更新祖先节点的平衡因子,而这一回溯过程的唯一途径就是通过 _parent 指针链。

4. 平衡因子 _bf

int _bf(balance factor)是 AVL 树区别于普通二叉搜索树的核心成员。其定义为:

_bf = 右子树高度 - 左子树高度

对于一棵平衡的 AVL(子)树,该值只能取 -1、0 或 1。一旦某节点的平衡因子超出这个范围(变成 -2 或 2),说明该子树失衡,必须立即触发旋转操作进行修正。

构造函数

构造函数通过初始化列表将各成员设为初始状态:

  • _kv 接收调用者传入的键值对;
  • 三个指针 _left_right_parent 全部置为 nullptr,表示新节点暂时孤立,待插入操作时挂接到树中;
  • _bf 初始化为 0------新插入的叶子节点没有左右子树,高度差为 0,合乎平衡定义。

整体设计思想

AVL 树节点的设计围绕一个核心目标:在插入/删除后能快速发现并修复失衡_parent 提供向上回溯的能力,_bf 则提供量化的平衡判断依据。这两个成员让 AVL 树能以 O(logN) 的代价维持自平衡,保证查找效率稳定在对数级别。

具体代码如下

cpp 复制代码
template<class K, class V>
struct AVLTreeNode
{
    pair<K, V> _kv;
    AVLTreeNode<K, V>* _left;    // 左孩子
    AVLTreeNode<K, V>* _right;   // 右孩子
    AVLTreeNode<K, V>* _parent;  // 父节点,用于回溯更新平衡因子
    int _bf;                     // 平衡因子 = 右子树高 - 左子树高
    AVLTreeNode(const pair<K, V>& kv)
        :_kv(kv)
        ,_left(nullptr)
        ,_right(nullptr)
        ,_parent(nullptr)
        ,_bf(0)
    {}
};

3.2插入

插入节点会让子树高度增加,插入节点也可能会影响部分祖先节点的平衡因子,至少会影响它的父亲节点。

更新平衡因子:

根据公式:平衡因子 = 右子树高度 - 左子树高度

(1)父节点存在右子树,且只有一个节点(此时平衡因子为1)

假设节点插入在父节点的右边,即父节点的右子树高度加一,父节点的左子树高度不变,即父节点的平衡因子也会加一,此时平衡因子为2,变为不平衡子树,此时需要另外一个操作:旋转,后续详讲;节点插入在父节点的左边,即父节点的左子树高度加一,父节点的右子树高度不变,根据公式即父节点的平衡因子会减一,此时平衡因子为0;而插入节点并没有左右子树,它的平衡因子为0。

(2)父节点存在左子树,且只有一个节点(此时平衡因子为-1)

假设节点插入在父节点的右边,即父节点的右子树高度加一,父节点的左子树高度不变,即父节点的平衡因子也会加一,此时平衡因子为0;节点插入在父节点的左边,即父节点的左子树高度加一,父节点的右子树高度不变,根据公式即父节点的平衡因子会减一,此时平衡因子为-2,变为不平衡子树,此时需要另外一个操作:旋转;而插入节点并没有左右子树,它的平衡因子为0。

更新停止条件:

(1)更新后parent的平衡因⼦等于0,更新中parent的平衡因⼦变化为-1->0或者1->0,说明更新前

parent⼦树⼀边⾼⼀边低,新增的结点插⼊在低的那边,插入后parent所在的⼦树⾼度不变,不会影响parent的⽗亲结点的平衡因⼦,更新结束。

(2)更新后parent的平衡因⼦等于1或-1,更新前更新中parent的平衡因⼦变化为0->1或者0->-1,说明更新前parent⼦树两边⼀样⾼,新增的插⼊结点后,parent所在的⼦树⼀边⾼⼀边低,parent所在的⼦树符合平衡要求,但是⾼度增加了1,会影响parent的⽗亲结点的平衡因⼦,所以要继续向上更新。

(3)更新后parent的平衡因子等于2或-2,更新前更新中parent的平衡因⼦变化为1->2或者-1->-2,说明更新前parent子树一边高一边低,新增的插⼊结点在高的那边,parent所在的子树高的那边更高了,破坏了平衡,parent所在的子树不符合平衡要求,需要旋转处理,旋转的目标有两个:1、把parent子树旋转平衡。2、降低parent⼦树的高度,恢复到插⼊结点以前的高度。所以旋转后也不需要继续往上更新,插⼊结束。

(4)不断更新,更新到根,跟的平衡因⼦是1或-1也停止了。

旋转:

原则:

(1)保持搜索树的规则

(2)让旋转后的树从不平衡变为平衡,并且降低高度

旋转总共有四种:右单旋、左单旋、左右双旋、右左双旋。

右单旋:左边高,向右旋

左单旋:右边高,向左旋

左右双旋:局部拆解,再(左右)单旋

右左双旋:局部拆解,再(右左)单旋

右单旋(左边高,向右旋)

触发条件: 在较高左子树的左侧插入新节点,导致 parent 的平衡因子变为 -2,且 child(parent->left)的平衡因子为 -1。

旋转过程:

  1. 将 child 的右子树(child->right)挂接到 parent 的左子树;

  2. 将 parent 变为 child 的右子树;

  3. 更新 child 和 parent 的平衡因子为 0。

    复制代码
      parent(-2)              child(0)
         /                      /  \
     child(-1)    ===>      T1   parent(0)
     /    \                       /
    T1    T2                    T2

说明: 上图中,childparent 的左孩子,T1T2 分别是 child 的左子树和右子树。旋转后 child 成为新的子树根,parent 成为 child 的右孩子,T2 被挂接到 parent 的左子树。整棵子树高度降低 1,恢复平衡。


cpp 复制代码
void RotateR(Node* parent)  // 右旋
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

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

	Node* parentParent = parent->_parent;

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

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

		subL->_parent = parentParent;
	}

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

左单旋(右边高,向左旋)

触发条件: 在较高右子树的右侧插入新节点,导致 parent 的平衡因子变为 2,且 child(parent->right)的平衡因子为 1。

旋转过程:

  1. 将 child 的左子树(child->left)挂接到 parent 的右子树;

  2. 将 parent 变为 child 的左子树;

  3. 更新 child 和 parent 的平衡因子为 0。

    复制代码
    parent(2)                    child(0)
       \                          /  \
      child(1)    ===>      parent(0)  T2
       /   \                      \
      T1   T2                     T1

说明: 上图中,childparent 的右孩子,T1T2 分别是 child 的左子树和右子树。旋转后 child 成为新的子树根,parent 成为 child 的左孩子,T1 被挂接到 parent 的右子树。整棵子树高度降低 1,恢复平衡。


cpp 复制代码
void RotateL(Node* parent)  // 左旋
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

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

	Node* parentParent = parent->_parent;

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

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

		subR->_parent = parentParent;
	}

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

左右双旋(先左旋,再右旋)

触发条件: 在较高左子树的右侧插入新节点,导致 parent 的平衡因子变为 -2,且 child(parent->left)的平衡因子为 1。

旋转过程(分两步):

  1. 先对 child 做左单旋 ,将 child->right 提升到 child 的位置;

  2. 再对 parent 做右单旋,将上一步得到的新子树根提升到 parent 的位置。

    复制代码
      parent(-2)           parent(-2)          newRoot(0)
         /                    /                   /  \
     child(1)    ===>    newRoot(0)    ===>   child  parent
     /    \               /    \               /  \    /  \
    T1   newRoot       child   T4            T1   T2 T3  T4
         /    \        /    \
        T2    T3      T1    T2

说明: 上图中,newRootchild 的右孩子。第一步左旋将 newRoot 提升到 child 的位置,T2 挂到 child 的右子树;第二步右旋将 newRoot 提升到 parent 的位置,T3 挂到 parent 的左子树。旋转后 newRoot 成为新的子树根,平衡因子根据插入位置(T2 或 T3)调整为 0、1 或 -1。


cpp 复制代码
void RotateLR(Node* parent)  // 左右双旋
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	int bf = subLR->_bf;

	RotateL(subL);
	RotateR(parent);

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

右左双旋(先右旋,再左旋)

触发条件: 在较高右子树的左侧插入新节点,导致 parent 的平衡因子变为 2,且 child(parent->right)的平衡因子为 -1。

旋转过程(分两步):

  1. 先对 child 做右单旋 ,将 child->left 提升到 child 的位置;

  2. 再对 parent 做左单旋,将上一步得到的新子树根提升到 parent 的位置。

    复制代码
    parent(2)              parent(2)              newRoot(0)
       \                       \                   /  \
      child(-1)    ===>      newRoot(0)    ===> parent  child
       /   \                  /    \            /  \    /  \
    newRoot  T4            T1    child         T1  T2  T3  T4
    /    \                      /    \

    T1 T2 T2 T3

说明: 上图中,newRootchild 的左孩子。第一步右旋将 newRoot 提升到 child 的位置,T2 挂到 child 的左子树;第二步左旋将 newRoot 提升到 parent 的位置,T3 挂到 parent 的右子树。旋转后 newRoot 成为新的子树根,平衡因子根据插入位置(T2 或 T3)调整为 0、1 或 -1。

cpp 复制代码
void RotateRL(Node* parent)  // 右左双旋
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	int bf = subRL->_bf;
	RotateR(parent->_right);
	RotateL(parent);
	if (bf == 0)
	{
		subR->_bf = 0;
		subRL->_bf = 0;
		parent->_bf = 0;
	}
	else if (bf == 1)
	{
		subR->_bf = 0;
		subRL->_bf = 0;
		parent->_bf = -1;
	}
	else if (bf == -1)
	{
		subR->_bf = 1;
		subRL->_bf = 0;
		parent->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

具体代码如下

cpp 复制代码
bool 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 (cur->_kv.first < kv.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_kv.first > kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else
		{
			return false;
		}
	}

	cur = new Node(kv);
	cur->_bf = 0;

	if (parent->_kv.first < kv.first)
	{
		parent->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}

	cur->_parent = parent;

	while (parent)
	{
		if (cur == parent->_right)
		{
			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)
		{
			// 旋转处理
			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)
			{
				RotateLR(parent);
			}
			else if (parent->_bf == 2 && cur->_bf == -1)
			{
				RotateRL(parent);
			}
			else
			{
				assert(false);
			}

			break;
		}
		else
		{
			// 插入之前这棵树就有2/-2 bf的节点,这棵树之前就不是AVL树
			assert(false);
		}
	}

	return true;
}

删除操作这里不会再讲,因为本篇博客重点是阐述平衡因子和旋转操作,删除操作也是在搜索二叉树的删除操作(替换删除)上维持平衡因子和进行旋转操作

如需深入了解 AVL 树的删除操作,推荐阅读 CSDN 博主「理工小羊」的《【数据结构】AVL树的删除(解析有点东西哦)》,该文对删除操作的四种旋转场景及平衡因子更新做了非常详尽的图解与代码实现,是学习 AVL 树删除的优质参考资料。

著作权声明: 本文由博主原创,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

四、总结:AVL树维持平衡的两大要素

AVL 树之所以能在插入后始终保持 O(logN) 的查找效率,核心在于它围绕 平衡因子旋转操作 这两个要素构建了一套闭环机制。

4.1 要素一:平衡因子------失衡的「探测器」

平衡因子是每个节点上维护的一个整数值,定义为 右子树高度 - 左子树高度,合法取值为 -1、0、1

核心思路:

  • 插入新节点后,通过 _parent 指针自底向上逐层更新祖先节点的平衡因子;
  • 一旦某节点的平衡因子变为 2 或 -2,立即判定该子树失衡,触发旋转;
  • 平衡因子变为 0 则停止更新(子树高度未变,不影响上层);
  • 平衡因子变为 1 或 -1 则继续向上更新(子树高度增加,可能影响祖先)。

提炼: 平衡因子是 AVL 树的「传感器」,它用 O(1) 的代价将「是否失衡」这一抽象问题量化为具体的数值判断,为旋转操作提供了精确的触发时机。

4.2 要素二:旋转操作------失衡的「修复器」

旋转是 AVL 树恢复平衡的具体手段,共有四种形态,但本质都是在保持二叉搜索树性质的前提下,重新分配节点位置以降低子树高度

旋转类型 触发条件(parent / child 的 bf) 核心动作 效果
右单旋 parent = -2, child = -1 child 提升为根,parent 成为 child 的右孩子 左子树过高 → 向右旋转降低左子树高度
左单旋 parent = 2, child = 1 child 提升为根,parent 成为 child 的左孩子 右子树过高 → 向左旋转降低右子树高度
左右双旋 parent = -2, child = 1 先左旋 child,再右旋 parent 左子树的右侧插入 → 两次单旋组合
右左双旋 parent = 2, child = -1 先右旋 child,再左旋 parent 右子树的左侧插入 → 两次单旋组合

核心思路:

  • 单旋解决「直线型」失衡(插入在较高子树的外侧);
  • 双旋解决「折线型」失衡(插入在较高子树的内侧),本质是两次单旋的组合;
  • 旋转完成后,被旋转子树的高度恢复到插入前的水平,因此无需继续向上更新平衡因子。

提炼: 旋转是 AVL 树的「执行器」,它用局部结构调整的代价(O(1) 次指针操作)换取整棵树的全局平衡,保证任何节点的左右子树高度差不超过 1。

4.3 闭环思路:探测 → 修复 → 恢复

AVL 树维持平衡的完整流程可以概括为一条闭环链路:

复制代码
插入节点 → 更新平衡因子(自底向上)
    ↓
平衡因子为 2 或 -2?  ──否──→ 继续向上更新或结束
    ↓ 是
触发旋转(单旋/双旋)
    ↓
子树高度恢复 → 停止更新 → 整棵树保持平衡

关键洞察:

  1. 平衡因子是「因」,旋转是「果」------没有平衡因子的量化判断,旋转就无从触发;
  2. 旋转是「手段」,平衡是「目的」------旋转的最终目标不是旋转本身,而是让子树高度恢复到插入前的水平,从而保证上层祖先的平衡因子不受影响;
  3. 局部修复,全局稳定 ------每次旋转只影响从失衡节点到其子树的局部范围,但修复后整棵树的查找效率重新回到 O(logN)

4.4 一句话总结

AVL 树用平衡因子感知失衡,用旋转操作修复失衡,二者配合形成「探测 → 修复 → 恢复」的闭环,以 O(logN) 的代价实现了二叉搜索树的动态自平衡。