目录

前言

一. AVL树的概念

二. AVL树的实现

2.1 AVL树的的结构

2.2 AVL树的插入

2.2.1 插入过程

2.2.2 更新平衡因子

2.2.3 插入节点及更新平衡因子代码实现

2.3 旋转

2.3.1 旋转的原则

2.3.2 右单旋

2.3.3 右单旋代码实现

2.3.4 左单旋

2.3.5 左单旋代码实现

2.3.6 左右单旋

2.3.7 左右单旋代码实现

2.3.8 右左单旋

2.3.9 右左单旋代码实现

2.4 AVL树的查找

2.5 AVL树的平衡检测

总结


前言

        在二叉搜索树(BST)中,如果数据有序或接近有序插入,树会退化为单支树,查找效率从 O(\log N) 退化为 O(N)。为了解决这个问题,1962 年 G.M. Adelson-Velsky 和 E.M. Landis 发明了 AVL 树 。本文将详细讲解 AVL 树的核心概念、插入逻辑以及旋转机制的 C++ 实现。


一. AVL树的概念

  • AVL树是最先发明的自平衡二叉查找树,AVL是一颗空树,或者具备下列性质的二叉搜索树:它的 左右子树都是AVL树,且左右子树的高度度差的绝对值不超过1。AVL树是一颗高度度平衡搜索二叉树, 通过控制高度差去控制平衡。
  • 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
  • AVL树整体结点数量和分布和完全二叉树类似,高度可以控制在logN ,那么增删查改的效率也可 以控制在O(logN) ,相比二叉搜索树有了本质的提升。


二. AVL树的实现

2.1 AVL树的的结构

template<class K, class V>
struct AVLTreeNode
{
	// 需要parent指针,后续更新平衡因⼦可以看到 
	pair<K, V> _kv;
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	int _bf; // balance factor(平衡因子)

	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0) // 平衡因子
	{}
};

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

2.2 AVL树的插入

2.2.1 插入过程

  1. 插入一个值按二叉搜索树规则进行插入。
  2. 新增结点以后,只会影响祖先结点的高度,也就是可能会影响部分祖先结点的平衡因子,所以更新从新增结点 -> 根结点路径上的平衡因子,实际中最坏情况下要更新到根,有些情况更新到中间就可以停止了,具体情况我们下面再详细分析。
  3. 更新平衡因子过程中没有出现问题,则插入结束
  4. 更新平衡因子过程中出现不平衡,对不平衡子树旋转,旋转后本质调平衡的同时,本质降低了子树的高度,不会再影响上一层,所以插入结束。

2.2.2 更新平衡因子

更新规则:

  • 平衡因子 = 右子树高度 - 左子树高度
  • 只有子树高度变化才会影响当前结点平衡因子。
  • 插入结点,会增加高度,所以新增结点在parent的右子树,parent的平衡因⼦++,新增结点在 parent的左子树,parent平衡因子--
  • parent所在子树的高度是否变化决定了是否会继续往上更新

更新停止的条件:

  • 更新后parent的平衡因子等于0,更新中parent的平衡因子变化为 -1->0 或者 1->0,说明更新前 parent子树一边高一边低,新增的结点插入在低的那边,插入后parent所在的子树高度不变,不会 影响parent的父亲结点的平衡因子,更新结束。
  • 更新后parent的平衡因子等于1或 -1,更新前更新中parent的平衡因子变化为 0->1 或者 0->-1,说 明更新前parent子树两边一样高,新增的插入结点后,parent所在的子树一边高一边低,parent所 在的子树符合平衡要求,但是高度增加了1,会影响parent的父亲结点的平衡因子,所以要继续向 上更新。
  • 更新后parent的平衡因子等于2或 -2,更新前更新中parent的平衡因⼦变化为1->2 或者 -1->-2,说 明更新前parent子树一边高一边低,新增的插入结点在高的那边,parent所在的子树高的那边更高了,破坏了平衡,parent所在的子树不符合平衡要求,需要旋转处理,旋转的目标有两个:1、把 parent子树旋转平衡。2、降低parent子树的高度,恢复到插入结点以前的高度。所以旋转后也不需要继续往上更新,插入结束。因为插入前这棵树肯定是符合要求的。
  • 不断更新,更新到根,跟的平衡因子是1或-1也停止了。

综上:先根据平衡因子判断高度变不变,在根据高度是否变化判断是否向上跟新或旋转


情况1:更新到10结点,平衡因子为2,10所在的子树已经不平衡,需要旋转处理
情况2:更新到中间结点,3为根的子树高度不变,平衡因子为0,不会影响上⼀层,更新结束
情况3:最坏更新到根停止,虽然平衡因子为-1,但是已经到了根节点,所以不会继续向上更新

2.2.3 插入节点及更新平衡因子代码实现

bool Insert(const pair<K, V>& kv)
{
    // 按BST的插入方法插入数据
	if (_root == nullptr)
	{
		_root = new Node(kv); // kv是包含K和V的结构对象
		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; // 不给也行,因为前面默认给的就是0
	if (parent->_kv.first < kv.first)
	{
		parent->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}
	cur->_parent = parent;

	// 控制平衡
	// 1、更新平衡因子
	// 平衡因子 = 右子树的高度 - 左子树的高度
	while (parent) // 二叉树中任意一个节点都有父亲,只有根没有,parent为空,说明更新到了根节点,这时就会结束。即使平衡因子是1/-1,也不会向上更新了
	{
		if (cur == parent->_left)
		{
			parent->_bf--;
		}
		if (cur == parent->_right)
		{
			parent->_bf++;
		}

		// 平衡因子更新后有3种情况
		if (parent->_bf == 0)
		{
			// 1.parent所在的子树高度不变,不会影响上一层,更新结束
			break;
		}
		else if (parent->_bf == 1 || parent->_bf == -1)
		{
			// 2.parent所在的子树高度变了,会影响在上一层,继续向上更新
			cur = parent;
			parent = parent->_parent;
		}
		else if (parent->_bf == 2 || parent->_bf == -2)
		{
			// 3.parent所在子树已经不平衡了,需要旋转处理(4种旋转)
			// 1、右单旋
			if (parent->_bf == -2 && cur->_bf == -1)
			{
				RotateR(parent); // 旋转点值parent
			}
			// 2、左单旋
			else if (parent->_bf == 2 && cur->_bf == 1)
			{
				RotateL(parent);
			}
			// 3、左右双旋
			else if (parent->_bf == -2 && cur->_bf == 1)
			{
				RotateLR(parent);
			}
			// 4、右左双旋
			else if (parent->_bf == 2 && cur->_bf == -1)
			{
				RotateRL(parent);
			}

			break; // 旋转之后就不需要继续向上更新了,这棵树就平衡了
		}
		else
		{
			assert(false); // 加一个false断言(更保险),如果有断言错误,说明之前这棵树就有问题,我们没有观测到
		}
	}

	return true;
}

2.3 旋转

2.3.1 旋转的原则

  1. 保持搜索树的规则
  2. 让旋转的树从不满足变平衡,其次降低旋转树的高度旋转总共分为四种:左单旋/右单旋/左右双旋/右左双旋。

说明:下面的图中,有些结点我们给的是具体值,如10和5等结点实际中是什么值都可以,只要大小关系符合搜索树的性质即可


2.3.2 右单旋

  • 本图1展示的是10为根的树,有a/b/c抽象为三棵高度为h的子树(h>=0),a/b/c均符合AVL树的要 求。10可能是整棵树的根,也可能是⼀个整棵树中局部的子树的根。这里a/b/c是高度为h的子树, 是一种概括抽象表示,他代表了所有右单旋的场景,实际右单旋形态有很多种,具体图2/图3/图4/ 图5进行了详细描述。
  • 在a子树中插入一个新结点,导致a子树的高度从 h 变成 h+1,不断向上更新平衡因子,导致10的平衡因子从 -1 变成 -2,10为根的树左右高度差超过1,违反平衡规则。10为根的树左边太高了,需要往右边旋转,控制两棵树的平衡。
  • 旋转核心步骤,因为5 < b子树的值 < 10,将b变成10的左子树,10变成5的右子树,5变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的高度恢复到了插入之前的 h+2,符合旋转原 则。如果插入之前10是整棵树的一个局部子树,旋转后不会再影响上一层,插入就结束了。

抽象表达出所有情况的图

具体情况的几个场景


2.3.3 右单旋代码实现

// 右单旋
void RotateR(Node* parent) // 旋转点
{
	Node* subL = parent->_left; // parent的左子树
	Node* subLR = subL->_right; // parent左子树的右子树

	// b变为10的左边
	parent->_left = subLR;
	if(subLR)
		subLR->_parent = parent; // subLR可能为空

	// 记录父节点的父节点
	Node* parentParent = parent->_parent;

	// 10变为5的右边
	subL->_right = parent; 
	parent->_parent = subL;

	// 让5变为这个树新的根
	if (parent == _root) // parent是整棵树根节点
	{
		_root = subL;
		subL->_parent = nullptr;
	}
	else // parent是一个局部的子树的根节点,要与上一层链接
	{
		if (parentParent->_left == parent)
		{
			parentParent->_left = subL;
		}
		else
		{
			parentParent->_right = subL;
		}
		subL->_parent = parentParent;
	}

	// 更新平衡因子
	parent->_bf = subL->_bf = 0;
}

2.3.4 左单旋

  • 本图6展示的是10为根的树,有a/b/c抽象为三棵高度为h的子树(h>=0),a/b/c均符合AVL树的要求。10可能是整棵树的根,也可能是一个整棵树中局部的子树的根。这里a/b/c是高度为h的子树, 是一种概括抽象表示,他代表了所有右单旋的场景,实际右单旋形态有很多种,具体跟上面左旋类似。
  • 在a子树中插入一个新结点,导致a子树的高度从h变成h+1,不断向上更新平衡因子,导致10的平 衡因子从1变成2,10为根的树左右高度差超过1,违反平衡规则。10为根的树右边太高了,需要往左边旋转,控制两棵树的平衡。
  • 旋转核新步骤,因为10 < b子树的高度 < 15,将b变成10的右⼦树,10变成15的左子树,15变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的高度恢复到了插入之前的h+2,符合旋转原则。如果插入之前10是整棵树的一个局部子树,旋转后不会再影响上一层,插入就结束了。


2.3.5 左单旋代码实现

// 左单旋
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 (parentParent == nullptr)
	{
		_root = subR;
		subR->_parent = nullptr;
	}
	else
	{
		if (parent == parentParent->_left)
		{
			parentParent->_left = subR;
		}
		else
		{
			parentParent->_right = subR;
		}
		subR->_parent = parentParent;
	}
	parent->_bf = subR->_bf = 0;
}

2.3.6 左右单旋

通过下图可以看到,左边高时,如果插入位置不是在a子树,而是插入在b子树,b子树高度从h变 成h+1,引发旋转,右单旋无法解决问题,右单旋后,我们的树依旧不平衡。右单旋解决的纯粹的左边高,但是插入在b子树中,10为跟的子树不再是单纯的左边高,对于10是左边高,但是对于5是右边高,需要用两次旋转才能解决,以5为旋转点进行一次左单旋,以10为旋转点进行一次右单旋,这棵树就平衡了。


上图分别为左右双旋中h==0和h==1具体场景分析,下面我们将a/b/c子树抽象为高度h的AVL子树进行分析,另外我们需要把b子树的细节进一步展开为8和左子树高度为h-1的e和f子树,因为我们要对b的父亲5为旋转点进行左单旋,左单旋需要动b树中的左子树。b子树中新增结点的位置不同,平衡因子更新的细节也不同,通过观察8的平衡因子不同,这里我们要分三个场景讨论

  • 场景1:h >= 1时,新增结点插入在e子树,e子树高度从h-1并为h并不断更新8->5->10平衡因子, 引发旋转,其中8的平衡因子为-1,旋转后8和5平衡因子为0,10平衡因子为1。
  • 场景2:h >= 1时,新增结点插入在f子树,f子树高度从h-1变为h并不断更新8->5->10平衡因子,引发旋转,其中8的平衡因子为1,旋转后8和10平衡因子为0,5平衡因子为-1。
  • 场景3:h == 0时,a/b/c都是空树,b自己就是⼀个新增结点,不断更新5->10平衡因子,引发旋转,其中8的平衡因子为0,旋转后8和10和5平衡因子均为0。


2.3.7 左右单旋代码实现

// 左右双旋
void RotateLR(Node* parent)
{
	// 记录旋转前的平衡因子,防止旋转后平衡因子变化
	Node* subL = parent->_left;
	Node* subLR = parent->_left->_right;
	int bf = subLR->_bf;

	// 复用
	RotateL(parent->_left);
	RotateR(parent);

	// 3种情况更新平衡因子
	// 1、这个节点就是新增节点
	if (bf == 0)
	{
		parent->_bf = 0;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
	// 2、在f插入的
	else if (bf == 1)
	{
		parent->_bf = 0;
		subL->_bf = -1;
		subLR->_bf = 0;
	}
	// 3、在e插入
	else if (bf == -1)
	{
		parent->_bf = 1;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
	else
	{
		assert(false); // 如果平衡因子出现其他情况,那肯定是其他情况出大问题了,断言一下
	}
}

2.3.8 右左单旋

跟左右双旋类似,下面我们将a/b/c子树抽象为高度h的AVL子树进行分析,另外我们需要把b子树的 细节进一步展开为12和左子树高度为h-1的e和f子树,因为我们要对b的父亲15为旋转点进行右单 旋,右单旋需要动b树中的右子树。b子树中新增结点的位置不同,平衡因子更新的细节也不同,通 过观察12的平衡因子不同,这里我们要分三个场景讨论:

  • 场景1:h >= 1时,新增结点插入在e子树,e子树高度从h-1变为h并不断更新12->15->10平衡因子,引发旋转,其中12的平衡因子为-1,旋转后10和12平衡因子为0,15平衡因子为1。
  • 场景2:h >= 1时,新增结点插入在f子树,f子树高度从h-1变为h并不断更新12->15->10平衡因子, 引发旋转,其中12的平衡因子为1,旋转后15和12平衡因子为0,10平衡因子为-1。
  • 场景3:h == 0时,a/b/c都是空树,b自己就是一个新增结点,不断更新15->10平衡因子,引发旋转,其中12的平衡因子为0,旋转后10和12和15平衡因子均为0。


2.3.9 右左单旋代码实现

// 右左双旋
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);
	}
}

2.4 AVL树的查找

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

Node* 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;
}

2.5 AVL树的平衡检测

我们实现的AVL树是否合格,我们通过检查左右子树高度差的的程序进行反向验证,同时检查一下结点的平衡因子更新是否出现了问题。

template<class K,class V>
class AVLTree
{
public:
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

	Node* 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;
	}

	// 外面不方便传根节点,所以套一层
	bool IsBalanceTree()
	{
		return _IsBalanceTree(_root);
	}

	int Height()
	{
		return _Height(_root);
	}

	int Size()
	{
		return _Size(_root);
	}

private:
	int _Size(Node* root)
	{
		if (root == nullptr)
			return 0;

		return _Size(root->_left) + _Size(root->_right) + 1;
	}

	// 算高度
	int _Height(Node* root)
	{
		if (root == nullptr)
			return 0;
		int leftHeight = _Height(root->_left);
		int rightHeight = _Height(root->_right);
		// 左右子树中高度更高的子树高度加1
		return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1; 
	}

	bool _IsBalanceTree(Node* root)
	{
		// 空树也是AVL树 
		if (nullptr == root)
			return true;
		// 计算pRoot结点的平衡因⼦:即pRoot左右⼦树的高度差 
		int lh = _Height(root->_left);
		int rh = _Height(root->_right);
		// 如果计算出的平衡因⼦与pRoot的平衡因⼦不相等,或者 
		// pRoot平衡因子的绝对值超过1,则⼀定不是AVL树 
		if (rh-lh != root->_bf || abs(root->_bf) >= 2)
		{
			cout << root->_kv.first << "平衡因子异常" << endl;
			return false;
		}

		// pRoot的左和右如果都是AVL树,则该树⼀定是AVL树 
		return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
	}

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

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

private:
	Node* _root = nullptr;
};

总结

AVL 树通过引入平衡因子旋转机制,完美解决了二叉搜索树在极端数据下的退化问题。

  • 优点:严格平衡,查找效率极高,稳定在 $O(\log N)$。

  • 缺点:插入和删除时需要频繁进行旋转维护平衡,实现较为复杂。


结语

文章如有不足或改进之处,欢迎大家在评论区积极讨论,后续我也会持续更新C++相关的知识。文章制作不易,如果文章对你有帮助,就点赞收藏关注支持一下作者吧,让我们一起努力,共同进步!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐