AVL树 基本操作 C++ 实现

相较于红黑树 AVL虽然次了点 但是同样重要 以下是个人简单实现的AVL树的思路和过程

遇上的问题 碰上的重点 都在代码注释里注明

一、节点结构 AVLTreeNode<K, V>

  • 成员pair<K,V> _kv(键值对)、三叉链(_left_right_parent)、平衡因子 _bf

  • 构造函数 :初始化所有指针为 nullptr_bf 置 0

cpp 复制代码
template <class K,class V>
struct AVLTreeNode
{
	pair<K, V> _kv;//这个用来存键值对的first 和 second

	AVLTreeNode<K,V>* _left;
	AVLTreeNode<K,V>* _right;
	AVLTreeNode<K,V>* _parent;

	int _bf;

	AVLTreeNode(const pair<K,V>& kv)//初始化 或者赋值的必要步骤-----要了解AVL树的节点配置
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{ }
};

二、核心操作:插入 Insert(pair<K,V> kv)

  1. 空树特判:直接创建根节点

  2. BST 插入 :按键值大小找到插入位置,维护 parent 指针

  3. 链接新节点 :将新节点挂到 parent 的左或右,设置 _parent

  4. 更新平衡因子 :从 parent 向上循环,根据 child 是左/右孩子决定 _bf--_bf++

  5. 判断并旋转

    • _bf == 0:树平衡,退出

    • _bf == ±1:继续向上更新

    • _bf == ±2:根据 child->_bf 选择单旋或双旋,旋转后退出

cpp 复制代码
bool Insert(pair<K, V> kv)
{
	Node* cur = _root;
	Node* parent = nullptr;

	if (_root == nullptr)//插入第一个节点 中没有节点比较 要注意对空指针解引用的操作-需要特殊处理
	{
		_root = new Node(kv);
		return true;
	}
		

	//先找到位置再进行插入  
	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;//相等的不要
		}
	}
	//找到位置之后进行插入 插入左边或者右边---同样的我把旋转也因此变成两个部分维护
	//保留前面的搜索二叉树的逻辑 理解更简单
	//理解此时的parent在哪里
	cur = new Node(kv);//把cur来充当新节点 方便寻找
	if (kv.first < parent->_kv.first)//两种情况直接合并一起操作---这些简化代码的片段要敏感可能后续会有体会以至于可以对未来做铺垫
	{
		parent->_left = cur;
		cur->_parent = parent;
	}
	else
	{
		parent->_right = cur;
		cur->_parent = parent;
	}

	Node* child = cur;//用来后面旋转子树的定位点

	while (parent)
	{
		if (child == parent->_left) 
		{
			parent->_bf--;// 插入到左子树
		}
		else 
		{ // child == parent->_right
			parent->_bf++;// 插入到右子树
		}
		if (parent->_bf == 0)
		{
			break;
		}
		else if (parent->_bf == -1|| parent->_bf == 1)
		{
			child = parent;
			parent = parent->_parent;
		}
		else if (parent->_bf == -2|| parent->_bf == 2)
		{
			//旋转
			if (parent->_bf == -2)
			{
				if (child->_bf == -1)	RotateR(parent);
				else if (child->_bf == 1)	RotateLR(parent);
			}
			else
			{
				if (child->_bf == 1)	RotateL(parent);
				else if (child->_bf == -1)	RotateRL(parent);						
			}
			break;//旋转完就直接跳出循环
		}
	}
	return true;		
}

三、旋转操作(四种)

函数 触发条件 作用
RotateR(parent) parent->_bf == -2 && child->_bf == -1 右旋(LL 型)
RotateL(parent) parent->_bf == 2 && child->_bf == 1 左旋(RR 型)
RotateLR(parent) parent->_bf == -2 && child->_bf == 1 左右旋(LR 型)
RotateRL(parent) parent->_bf == 2 && child->_bf == -1 右左旋(RL 型)
cpp 复制代码
	void RotateR(Node* parent)//没必要加& 加了后续不使用的倒也不影响
	{
		//保存数据
		Node* parentParent = parent->_parent;//传过来的也有可能是子树 所以pP还有可能存在的 就算是不存在也是nullptr
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		//连接数据
		subL->_right = parent;
		parent->_parent = subL;
		subL->_parent = parentParent;//双向链接不要搞忘了

		if (parentParent)//需要考虑 parent可能是parentParent的左或者右 同样需要分情况
		{
			if (parent == parentParent->_left)
				parentParent->_left = subL;
			else
				parentParent->_right = subL;
		}
		else
		{
			_root = subL;//如果parent是根节点 跟新根
		}


		// 这里要无条件赋值
		parent->_left = subLR;//是空的也没关系 就像手动置空一样
		if (subLR) 
		{
			subLR->_parent = parent;
		}
		else 
		{
			parent->_left = nullptr; // 显式置空,防止残留
		}
		
		//处理平衡因子

		subL->_bf = 0;
		parent->_bf = 0;
	}
cpp 复制代码
void RotateL(Node* parent)
{
	//保存数据
	Node* parentParent = parent->_parent;//传过来的也有可能是子树 所以pP还有可能存在的 就算是不存在也是nullptr
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	//连接数据
	subR->_left = parent;
	parent->_parent = subR;
	subR->_parent = parentParent;//双向链接不要搞忘了

	if (parentParent)//需要考虑 parent可能是parentParent的左或者右 同样需要分情况
	{
		if (parent == parentParent->_left)
			parentParent->_left = subR;
		else
			parentParent->_right = subR;
	}
	else
	{
		_root = subR;//如果parent是根节点 跟新根
	}

	
	// 这里要无条件赋值
	parent->_right = subRL;
	if (subRL)
	{
		subRL->_parent = parent;
	}
	else
	{
		parent->_right = nullptr;   // 显式置空,防止残留
	}
	//处理平衡因子

	subR->_bf = 0;
	parent->_bf = 0;
	//parent->_bf == 2(右子树比左子树高 2)
	//subR->_bf == 1(右孩子的右子树更高)
	//此时 subRL(即 subR 的左孩子)的高度 一定等于 parent 的左子树高度,否则不会触发失衡
	//在旋转前,subRL 的高度与 parent->_left 的高度相同
}
cpp 复制代码
	void RotateLR(Node* parent)
	{
		//保存数据
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		int bf = subLR->_bf;//记录插入后的subL 的平衡因子

		//左右旋转
		RotateL(subL);
		RotateR(parent);

		//调节平衡因子
		//旋转第一次可能并不是正常的 第二次旋转后就会达到正常效果 然后再根据subLR调整平衡因子
		if (bf == 0)//忘了为啥就再去画一遍 就知道了
		{
			subL->_bf = 0;
			subLR->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == 1)
		{
			subLR->_bf = 0;
			subL->_bf = -1;
			parent->_bf = 0;
		}
		else if (bf == -1)
		{
			subLR->_bf = 0;
			subL->_bf = 0;
			parent->_bf = 1;
		}
		else
			assert(false);
	}
cpp 复制代码
void RotateRL(Node* parent)
{
	//保存数据
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	int bf = subRL->_bf;//记录插入后的subL 的平衡因子

	//右左旋转
	RotateR(subR);
	RotateL(parent);

	//调节平衡因子
	//旋转第一次可能并不是正常的 第二次旋转后就会达到正常效果 然后再根据subLR调整平衡因子
	if (bf == 0)//parent到左边来了
	{
		subR->_bf = 0;
		subRL->_bf = 0;
		parent->_bf = 0;
	}
	else if (bf == 1)
	{
		subRL->_bf = 0;
		subR->_bf = 0;
		parent->_bf = -1;
	}
	else if (bf == -1)
	{
		subRL->_bf = 0;
		subR->_bf = 1;
		parent->_bf = 0;
	}
	else
		assert(false);
}

旋转函数内部都做了

  • 保存关键节点(subL/subRsubLR/subRL

  • 调整指针链接(含父指针更新)

  • 更新平衡因子(单旋置 0,双旋根据 bf 分支调整)

四、辅助功能

函数 说明
InOrder() 中序遍历,输出有序序列,用于验证 BST 性质
Find(const K& key) 按键值查找节点,返回节点指针或 nullptr
Height() 递归计算树的高度
Size() 递归计算节点个数
IsBalanceTree() 检查每个节点的 _bf 是否等于其左右子树高度差(右-左),并确保 `
cpp 复制代码
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

		_InOrder(root->_left);
		cout << root->_kv.first << " ";
		//cout << root->_kv.first << ":" << root->_kv.second << endl;
		_InOrder(root->_right);
	}

	int _Size(Node* root)
	{
		return root == nullptr ? 0 : _Size(root->_left) + _Size(root->_right) + 1;
	}

	int _Height(Node* root)
	{
		if (root == nullptr)//没有就直接返回 0 
			return 0;
		int leftHeight = _Height(root->_left);
		int rightHeight = _Height(root->_right);
		return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;//后面还有就直接放回 1
	}

	bool _IsBalanceTree(Node* root)//检查是否平衡 看左右树的高度差值绝对值
	{
		if (root == nullptr)
			return true;

		//if (root->_kv.first == 7)//这个用来调试的
		//{
		//	int s = 55;
		//}
		int leftHeight = _Height(root->_left);
		int rightHeight = _Height(root->_right);
		int bf = rightHeight - leftHeight;

		if (abs(bf) >= 2 || bf != root->_bf)
		{
			cout << root->_kv.first <<" " << "平衡因子异常" << endl;
			return false;
		}//不符合要求在这里体现
		return _IsBalanceTree(root->_left)
			&& _IsBalanceTree(root->_right);//依次检查 左右子树
	}

注意事项

  1. 平衡因子定义右子树高度 - 左子树高度,与检查函数一致

  2. 旋转函数中的置空 :当 subLRsubRL 为空时,必须显式将 parent->_leftparent->_rightnullptr,防止残留指针

  3. 父指针更新 :每次指针变动都要同步更新 _parent,否则树结构会断裂

  4. 双旋中 bf 的取值 :在 RotateLRRotateRL 中,bf 记录的是 subLRsubRL 旋转前的平衡因子,用于决定最终各节点的 _bf

AVL树面试常见考点整理

一、基础概念

问题 回答要点
什么是 AVL 树? 自平衡的二叉搜索树,任意节点的左右子树高度差(平衡因子)绝对值 ≤ 1
平衡因子如何定义? 通常有两种定义:左-右 或 右-左 代码中用的是 右 - 左
AVL 树的高度有什么保证? 树高严格控制在 O(log n),保证查找、插入、删除均为 O(log n)
AVL 树与普通 BST 的区别? BST 都在一边可能退化为链表(O(n)),AVL 通过旋转保持平衡

二、旋转操作

问题 回答要点
什么时候需要旋转? 插入或删除后,某节点的平衡因子变为 ±2 时
有哪四种旋转? LL(右旋)、RR(左旋)、LR(左右旋)、RL(右左旋)
如何判断用哪种旋转? 检查失衡节点 parent->_bf 和它的孩子 child->_bf
旋转过程中需要注意什么? 父指针更新、子树的空指针置空、平衡因子重置
手写一个旋转的伪代码。 常见手撕题目,必须能完整写出至少一种旋转

面试高频题

在 AVL 树中插入节点 16, 3, 7,画出旋转过程


三、插入操作 代码实现

问题 回答要点
AVL 树插入的步骤? 1. BST 插入 → 2. 更新平衡因子 → 3. 检测失衡并旋转
更新平衡因子的逻辑? 从插入位置向上回溯,左子树插入 → _bf--,右子树插入 → _bf++
什么时候停止更新? 当某节点 _bf == 0 时,更高的祖先不再受影响,可以提前退出
插入的时间复杂度? O(log n),因为最多旋转一次(单旋或双旋),回溯高度为 log n

四、删除操作

虽然没实现删除,但面试官可能会问:

问题 回答要点
删除的步骤? 1. BST 删除 → 2. 从删除位置向上更新平衡因子 → 3. 失衡时旋转
删除后可能导致多次旋转? 是的 删除可能需要向上连续旋转多次(与插入不同,插入最多旋转一次)
删除时如何找到替代节点? 用左子树最大节点或右子树最小节点替换
删除的时间复杂度? O(log n),但可能需要多次旋转

五、性能与对比(高频横向比较)

问题 回答要点
AVL 树 vs 红黑树? AVL 查找更快(更平衡),红黑树插入/删除更快(旋转次数少)
AVL 树 vs 哈希表? 哈希表查找 O(1) 但无序;AVL 树有序且范围查询方便
为什么 C++ STL 用红黑树而不是 AVL? STL 的 map/set 插入删除操作更频繁,红黑树整体性能更优
AVL 树的高度能保证多少? 最坏情况高度 ≈ 1.44 * log₂(n)(平衡因子为 ±1 时)

六、常见面试题(手撕/思路)

  1. 实现 AVL 树的插入函数

  2. 判断一棵树是否为 AVL 树

  3. 计算 AVL 树的高度

  4. 找出 AVL 树中第 k 小的元素(可在节点上维护子树大小)

  5. AVL 树中插入 1~7,画出最终树形并标出平衡因子

我的gitee仓库

不负岁月无痕. (jiangmingpeng0716) - Gitee.com