在平衡中追寻高度:探秘AVL树的自我调节之美

文章目录

  • 前言
    • 🎓一、AVL树的概念
      • [1.1 定义与性质](#1.1 定义与性质)
      • [1.2 平衡因子](#1.2 平衡因子)
      • [1.3 旋转操作](#1.3 旋转操作)
    • [🎓二、AVL 树结点的定义](#🎓二、AVL 树结点的定义)
    • [🎓三、AVL 树的框架](#🎓三、AVL 树的框架)
    • [🎓四、AVL 树的插入](#🎓四、AVL 树的插入)
      • [4.1 平衡因子的更新](#4.1 平衡因子的更新)
      • [4.2 AVL 树的旋转](#4.2 AVL 树的旋转)
        • [4.2.1 左单旋](#4.2.1 左单旋)
        • [4.2.2 右单旋](#4.2.2 右单旋)
        • [4.2.3 右左双旋](#4.2.3 右左双旋)
        • [4.2.4 左右双旋](#4.2.4 左右双旋)
      • [4.3 插入过程的完整代码](#4.3 插入过程的完整代码)
      • [4.4 检查平衡](#4.4 检查平衡)
    • [🎓五、AVL 树的删除](#🎓五、AVL 树的删除)
    • [🎓六、AVL 树的性能](#🎓六、AVL 树的性能)
  • 结语

前言

继上篇C++探索之旅:打造高效二叉搜索树的奥秘与实践,我们继续探讨二叉搜索树的PLUS版------AVL树

在数据结构的世界里,树木生长的过程并非一帆风顺。如何在高度与平衡间取得微妙的和谐?AVL树,这个优雅的自平衡二叉搜索树,便是自然之道的程序化呈现。它在每次插入与删除中,仿佛有一双看不见的手,悄然调整,维持着一种动态的平衡,使得查找效率始终如一。本文将带您深入探究AVL树的构造、平衡因子、旋转操作,让每一行代码都成为追寻平衡的过程。


🎓一、AVL树的概念

AVL树的概念可以从以下几个方面进行阐述:

1.1 定义与性质

AVL树是一种自平衡二叉查找树 ,它的任何节点的两个子树的高度最大差别为1,因此也被称为高度平衡树 。AVL树的增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。AVL树得名于它的发明者G.M. Adelson-Velsky和E.M. Landis。AVL树具有二叉查找树的全部特性,每个节点的左子树的高度和右子树高度差值小于等于1 。左旋是AVL树中的一种操作,它是逆时针旋转两个节点,原先的右节点成为新的父节点,原先的父节点成为原先的右节点的左子节点。右旋是左旋的镜像操作。AVL树的查找、插入和删除在平均和最坏情况下的时间复杂度都是O(log n)

1.2 平衡因子

在AVL树中,每个节点都有一个平衡因子(Balance Factor) ,它表示该节点的右子树高度减左子树高度的差 。平衡因子的值可以是**-1、0或1**。当插入或删除节点导致某个节点的平衡因子超出这个范围时,就需要进行旋转操作来恢复树的平衡。

  • 如果一颗二叉搜索树是高度平均的,它就是 AVL 树。如果它有 n 个结点,其高度可保持在 O ( log2^n^ ),搜索时间复杂度为 O(log2^n^ )。平衡二叉搜索树中平衡并不是说要让平衡因子始终保持 0,这是做不到的,以两个结点为例,根节点的平衡因子只可能是 1 或者 -1,不可能是 0。二叉搜索树在实际中基本不太可能实现完全平衡,但是理论上可以,即满二叉树。后面我们学的多叉平衡搜索树在现实中可以做到完全平衡。

1.3 旋转操作

AVL树的平衡是通过四种旋转操作来实现的:

  1. 左旋转(Left Rotation):当某个节点的右子树高度较高时,通过左旋转来降低右子树的高度。
  2. 右旋转(Right Rotation):当某个节点的左子树高度较高时,通过右旋转来降低左子树的高度。
  3. 左右旋转(Left-Right Rotation):当某个节点的左子树的右子树高度较高时,通过先对左子树进行左旋转,再对根节点进行右旋转来调整树的平衡。
  4. 右左旋转(Right-Left Rotation):当某个节点的右子树的左子树高度较高时,通过先对右子树进行右旋转,再对根节点进行左旋转来调整树的平衡。

🎓二、AVL 树结点的定义

cpp 复制代码
template<class K, class V>
class AVLTreeNode {
public:
	pair<K, V> _kv;				//存储key和value
	AVLTreeNode<K, V>* left;	//指向左孩子
	AVLTreeNode<K, V>* right;	//指向右孩子
	AVLTreeNode<K, V>* _parent;	//指向父亲结点
    int bf = 0;				// 平衡因子

	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		,left(nullptr)
		,right(nullptr)
		,_parent(nullptr)
	{}
};

🎓三、AVL 树的框架

cpp 复制代码
template<class K, class V>
class AVLTree{
	typedef AVLTreeNode<K, V> Node;
    typedef pair<K, V> pr;
public:
    // ...成员函数
private:
	Node* _root = nullptr;
};

🎓四、AVL 树的插入

AVL 树就是在二叉搜索树的基础上引入平衡因子,因此 AVL 树也可以看成是二叉搜索树。那么 AVL 树的插入过程可以分为两步:

  1. 按照二叉搜索树的方式插入新节点。

  2. 调整结点的平衡因子。

4.1 平衡因子的更新

新结点插入后,AVL 树的平衡性可能会遭到破坏,此时就需要更新平衡因子,并检测是否破坏了 AVL 树的平衡性。假设新插入的结点为 cur,那么 cur 的平衡因子一定是 0,因为它的左后孩子都是 nullptr。但是 cur 的双亲结点 parent 的平衡因子一定需要调整,在插入之前,parent 的平衡因子分为三种情况:-1、0、1。对 parent 平衡因子的调整分为以下两种情况:

  • 如果 cur 插入到 parent 的左侧,只需要给 parent 的平衡因子 − 1 即可。

  • 如果 cur 插入到 parent 的右侧,只需要给 parent 的平衡因子 + 1 即可。

此时更新后 parent 的平衡因子有以下三种情况:0 、± 1 、± 2

  • 如果 parent 的平衡因子为 0 ,说明插入前 parent 的平衡因子一定为 ± 1 ,即左右孩子中一定有一个是 nullptr,并且新结点就插入在为空的一侧,插入后平衡因子被调整成 0 ,整棵树的高度任然不变,满足 AVL 树的性质,插入成功。

  • 如果 parent 的平衡因子是 ± 1 ,说明插入前 parent 的平衡因子一定是 0 ,即插入前 parent 的左右孩子都为 nullptr,并未新结点就插入在 parent 的任意一侧,所以插入后平衡因子被调整成了 ± 1 ,此时以 parent 为根的树的高度增加了,需要继续沿着祖先结点向上更新,知道某一个祖先节点的平衡因子变成了 0 ,此时更新结束,插入成功。

  • 如果 parent 的平衡因子为 ± 2 ,则 parent 的平衡因子违反了平衡树的性质,需要对其进行旋转处理。

4.2 AVL 树的旋转

如果在一颗原本是平衡的 AVL 树中插入一个新结点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据结点插入位置的不同,AVL 树的旋转分为四种:

4.2.1 左单旋

新结点插入在较高右子树的右侧----右右 :即 parent 的平衡因子是 2,cur 的平衡因子是 1 ,此时就需要左单旋。下面举两个左单旋的例子。

无论是这四种旋转中的哪一个,都要保证以下两点:首先在旋转的过程中要保证这棵树是搜索树,其次经过旋转后,这棵树应该变成平衡树,且降低这个子树的高度。这两点就决定了左旋的核心操作就是将 cur 的左子树给 parent 的右子树,再让 parent 成为 cur 的左子树。

cpp 复制代码
void RotateL(Node* parent) {
    // cur为parent的右子节点,将要成为新父节点
    Node* cur = parent->right;
    
    // cur的左子节点(可能为空),会变为parent的右子节点
    Node* curleft = cur->left;

    // 将parent的右子节点更新为cur的左子节点
    parent->right = curleft;
    // cur的左子节点更新为parent,完成旋转
    cur->left = parent;

    // 如果cur有左子节点,更新其父节点为parent
    if (curleft) curleft->_parent = parent;

    // 保存parent的父节点ppnode,旋转后cur成为新的父节点
    Node* ppnode = parent->_parent;
    parent->_parent = cur;

    // 如果parent是根节点,更新根节点为cur
    if (parent == _root) {
        _root = cur;
        cur->_parent = nullptr; // cur成为新的根节点,父节点为空
    } 
    // 否则更新ppnode的子节点指针
    else {
        // 如果parent是ppnode的左子节点,更新ppnode的左子节点为cur
        if (ppnode->left == parent) ppnode->left = cur;
        // 如果parent是ppnode的右子节点,更新ppnode的右子节点为cur
        else ppnode->right = cur; 

        // cur的父节点更新为ppnode
        cur->_parent = ppnode;
    }
    
    // 旋转后更新parent和cur的平衡因子
    parent->bf = cur->bf = 0;
}
4.2.2 右单旋

新结点插入在较高左子树的右侧----左左 :即 parent 的平衡因子是 2,cur 的平衡因子是 1 ,此时就需要右单旋。下面举出右单旋的抽象图。

cpp 复制代码
void RotateR(Node* parent) {
	Node* cur = parent->left;
	Node* curright = cur->right;

	parent->left = curright;
	cur->right = parent;

	if(curright) curright->_parent = parent;
	
	Node* ppnode = parent->_parent;
	parent->_parent = cur;

	if (parent == _root) {
		_root = cur;
		cur->_parent = nullptr;
	}
	else {
		if (ppnode->left == parent) ppnode->left = cur;
		else ppnode->right = cur;

		cur->_parent = ppnode;
	}
	
	parent->bf = cur->bf = 0;
}
4.2.3 右左双旋

下面以三种情况探讨右左双旋的奥秘。


代码片段:

cpp 复制代码
void RotateRL(Node* parent) {
	Node* cur = parent->right;
	Node* curleft = cur->left;
	int bf = curleft->bf;

	RotateR(parent->right);
	RotateL(parent);

	if (bf == 0) {
		cur->bf = curleft->bf = parent->bf = 0;
	}
	else if (bf == -1) {
		cur->bf = 1;
		curleft->bf = parent->bf = 0;
	}
	else if (bf == 1) {
		parent->bf = 1;
		curleft->bf = cur->bf = 0;
	}
	else assert(false);
}
4.2.4 左右双旋

下面以三种情况探讨左右双旋的奥秘。


代码片段:

cpp 复制代码
void RotateLR(Node* parent) {
	Node* cur = parent->left;
	Node* curright = cur->right;
	int bf = curright->bf;

	RotateL(parent->left);
	RotateR(parent);

	if (bf == 0) {
		cur->bf = parent->bf = curright->bf = 0;
	}
	else if (bf == -1) {
		parent->bf = -1;
		cur->bf = curright->bf = 0;
	}
	else if (bf == 1) {
		cur->bf = -1;
		curright->bf = parent->bf = 0;
	}
	else assert(false);
}

4.3 插入过程的完整代码

cpp 复制代码
bool insert(const pr& kv) {
	if (!_root) {
		_root = new Node(kv);
		return true;
	}

	// 定义当前节点cur
	Node* cur = _root;
	Node* parent = nullptr;
	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; // 不可能出现相等的情况插入
	}

	// 创建新的Node实例cur,并将它插入到适当的位置
	cur = new Node(kv);
	if (parent->_kv.first < kv.first) {
		parent->right = cur;
	}
	else {
		parent->left = cur;
	}

	cur->_parent = parent;

	// ... 控制平衡
	// 更新平衡因子
	while (parent) {
		//更新平衡因子
		if (cur == parent->left) {
			--(parent->bf);
		}
		else /*if (cur == parent->right)*/ {
			++(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 (cur->bf == 1 && parent->bf == 2) {
				RotateL(parent);
			}
			//右单旋
			else if (cur->bf == -1 && parent->bf == -2) {
				RotateR(parent);
			}
			//右左单旋
			else if (cur->bf == -1 && parent->bf == 2) {
				RotateRL(parent);
			}
			//左右单旋
			else if (cur->bf == 1 && parent->bf == -2) {
				RotateLR(parent);
			}

			break;
		}
		else assert(false);
	}

	return true;
}

4.4 检查平衡

AVL 树是在二叉搜索树的基础上加入了平衡的限制,因此要验证 AVL 树,可以分为两步:

  • 验证其为二叉搜索树。如果中序遍历可以得到一个有序的序列,就说明为二叉搜索树。

  • 验证其为平衡树。每个结点左右子树高度差的绝对值不超过1。其次检查结点的平衡因子是否计算正确。

cpp 复制代码
// 返回树的高度
int Height(Node* root) {
	if (!root) return 0;

	int leftHeight = Height(root->left);
	int rightHeight = Height(root->right);

	return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

// 调用检查平衡因子的函数
bool isBalance() {
	return isBalance(_root);
}

// 检查平衡因子
bool isBalance(Node* root){
	if (!root) return true;

	int leftHeight = Height(root->left);
	int rightHeight = Height(root->right);

	if (rightHeight - leftHeight != root->bf)
	{
		cout << "平衡因子异常:" << root->bf << endl;
		return false;
	}

	return abs(leftHeight - rightHeight) < 2
		&& isBalance(root->left)
		&& isBalance(root->right);
}

// 中序遍历
void inOrder(Node* root) {
	if (!root) return;

	inOrder(root->left);
	cout << root->_kv.first << " ";
	inOrder(root->right);
}

// 调用中序遍历的函数
void inOrder() {
	return inOrder(_root);
}
  • 注意:这里函数名相同却能成功调用的原因是构成了函数重载

测试代码:

cpp 复制代码
#include "AVLTree.h"

int main() {
	int a[] = { 16,23,5,7,8,6,9,14 };
	AVLTree<int, int> t;
	for (auto e : a) {
		t.insert(make_pair(e, e));
		t.inOrder();
		cout << endl;
		if (t.isBalance()) {
			cout << "插入成功" << endl;
		}
		else {
			cout << "插入失败" << endl;
		}
	}
	

	return 0;
}

输出:

🎓五、AVL 树的删除

因为 AVL 树也是二叉搜索树,可以按照二叉搜索树的方式将结点删除,然后再更新平衡因子,只不过与删除插入不同的是,删除结点后的平衡因子更新,最差情况下一直要调整到根结点的位置。具体的实现感兴趣的小伙伴可以参考《算法导论》或者《数据结构-用面向对象方法与C++描述》殷人昆版。

🎓六、AVL 树的性能

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


结语

AVL树向我们展示了一个动态平衡的世界,在每次操作后依旧屹立如初。它提醒我们,即使不断变化,也可以通过智慧与设计找到一种持久的稳定。愿这篇文章成为您在数据结构之路上的一盏灯,带您窥见代码背后那精妙的平衡哲学。

今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连 支持一下,17的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是17前进的动力!

相关推荐
说私域几秒前
完美日记营销模式对开源 AI 智能名片 2 + 1 链动模式 S2B2C 商城小程序的启示
人工智能·小程序
说私域8 分钟前
传统媒体终端移动化发展新趋势:融合开源 AI 智能名片与 S2B2C 商城小程序的创新探索
人工智能·小程序·零售
xm一点不soso16 分钟前
树莓派AI视觉小车--4.小车基本运动
人工智能·计算机视觉·视觉小车
hai4058719 分钟前
机器学习与成像技术
人工智能·机器学习
lzt232320 分钟前
深度学习中的 Dropout:原理、公式与实现解析
人工智能·python·深度学习·神经网络·transformer
shuyeah21 分钟前
LSTM结构原理
人工智能·rnn·lstm
跟德姆(dom)一起学AI25 分钟前
0基础跟德姆(dom)一起学AI 深度学习01-深度学习介绍
开发语言·人工智能·python·深度学习·机器学习
机器学习之心25 分钟前
粒子群优化双向深度学习!PSO-BiTCN-BiGRU-Attention多输入单输出回归预测
人工智能·深度学习·回归·多输入单输出回归预测·pso-bitcn-bigru
夜猫程序猿25 分钟前
RNN中的梯度消失与梯度爆炸问题
rnn·深度学习
IT猿手26 分钟前
基于卷积神经网络(CNN)的时间序列预测,15个输入1个输出,可以更改数据集,MATLAB代码
人工智能·深度学习·神经网络·算法·matlab·cnn