首页 > 编程语言 >算法与数据结构——AVL树(平衡二叉搜索树)

算法与数据结构——AVL树(平衡二叉搜索树)

时间:2024-09-04 16:27:10浏览次数:12  
标签:node 右旋 right TreeNode 二叉 AVL child 数据结构 节点

AVL树

在“二叉搜索树”章节提到,在多次插入和删除操作后,二叉搜索树可能退化为链表。在这种情况下,所有操作的时间复杂度将从O(logn)劣化为O(n)

如下图,经过两次删除节点操作,这棵二叉搜索树便会退化为链表

再例如,下图所示的完美二叉树中插入两个节点后,树将严重向左倾斜,查找操作的时间复杂度也随之劣化。

1962 年 G. M. Adelson‑Velsky 和 E. M. Landis 在 论 文 “An algorithm for the organization of information”中提出了 AVL 树。AVL树能够确保在持续添加和删除节点后不会退化,从而使得各种操作的时间复杂度保持在O(logn)级别。

AVL树常见术语

AVL树既是二叉搜索树,也是平衡二叉树,同时满足这两类二叉树的所有性质,因此是一种平衡二叉搜索树(balanced binary search tree)

节点高度

由于AVL树的相关操作需要获取节点高度,因此我们需要为节点类添加height变量:

/*AVL 树节点类*/
struct TreeNode{
	int val{};
	int height = 0;
	TreeNode *left{};
	TreeNode *right{};
	TreeNode() = default;
	explicit TreeNode(int x) :val(x){}
};

“节点高度”是指从该节点到它的最远叶节点的距离,即所经过的“边”的数量。需要特别注意的是,叶节点的高度为0,而空节点的高度为-1。我们将创建两个工具函数,分别用于获取和更新节点的高度:

/*获取节点高度*/
int AVLTree::height(TreeNode *node){
	return node == nullptr ? -1 : node->height;
}

/*更新节点高度*/
void AVLTree::updateHeight(TreeNode *node){
	// 节点高度等于最高子树高度 + 1
	node->height = max(height(node->left), height(node->right)) + 1;
}

节点平衡因子

节点的平衡因子(balance factor)定义为节点左子树的高度减去右子树的高度,同时规定空节点的平衡因子为0。我们同样将获取节点平衡因子的功能封装成函数,方便后续使用:

/*获取平衡因子*/
int AVLTree::balanceFactor(TreeNode *node){
	// 空节点 平衡因子为0
	if (node == nullptr)
		return 0;
	// 节点平衡因子 = 左子树高度 - 右子树高度
	return height(node->left) - height(node->right);
}

设平衡因子为f,则一棵AVL树的任意节点的平衡因子都满足 -1 < = f <= 1。

AVL树旋转

AVL树的特点在于“旋转”操作,它能够在不影响二叉树的中序遍历序列的前提下,使失衡节点重新回复平衡。换句话说,**旋转操作既能保持“二叉搜索树”的性质,也能使树重新变成“平衡二叉树”。

我们将平衡因子绝对值 > 1 的节点称为“失衡节点”。根据节点失衡的情况不同,旋转操作分为四种:右旋、左旋、先右旋后左旋、先左旋后右旋。

右旋

如图所示,节点下方为平衡因子。从底往顶看,二叉树中首个失衡节点时“节点3”。我们关注以该失衡节点为根节点的子树,将该节点记为node,其左子节点记为child,执行“右旋操作”。完成右旋后,子树恢复平衡,并且仍然保持二叉搜索树的性质。


另外当child节点有右子节点时(记为grand_child),需要在右旋中添加一步:将grand_child作为node的左子结点。

“右旋”是一种形象化的说法,实际上需要通过修改节点指针来实现。

/*右旋操作*/
TreeNode* AVLTree::rightRotate(TreeNode *node){
	TreeNode *child = node->left;
	TreeNode * grand_child = child->right;
	// 以child为原点,将 node 向右旋转
	child->right = node;
	node->left = grand_child;
	// 更新节点高度
	updateHeight(node);
	updateHeight(child);
	// 返回旋转后子树的根节点
	return child;
}

左旋

相应地,如果考虑上述失衡二叉树的“镜像”,则需要执行下图所示的“左旋”操作。

同理,当节点child有左子结点(记为grand_child)时,需要在左旋中添加一步grand_child作为node的右子节点。

观察发现,左旋和右旋操作在逻辑上是镜像对称的,它们分别解决的两种失衡情况也是对称的。基于对称性,我们只需要将右旋代码啊的所有left替换为right,将所有的right替换为left,即可得到左旋的实现代码:

/*左旋操作*/
TreeNode *AVLTree::leftRotate(TreeNode *node){
	TreeNode *child = node->right;
	TreeNode *grand_child = child->left;
	// 以child为原点 将 node 向左旋转
	child->left = node;
	node->right = grand_child;
	// 更新节点高度
	updateHeight(node);
	updateHeight(child);
	// 返回旋转后子树的根节点
	return child;
}

先左旋后右旋

下图中的失衡节点3,仅使用左旋或右旋都无法使子树恢复平衡。此时需要先对child执行“左旋”,再对node执行“右旋”。

先右旋后左旋

对于上述失衡二叉树的镜像情况,需要先对child执行“右旋”,再对node执行“左旋”操作。

旋转的选择

下面展示了四种失衡情况与上述案例逐个对应,分别需要采用右旋、先左旋后右旋、先右旋后左旋、左旋的操作。

如下表所示,我们通过判断失衡节点的平衡因子以及较高一侧子节点的平衡因子的正负号,来确定失衡节点属于那种情况

失衡节点的平衡因子 子节点的平衡因子 应采用的旋转方法
> 1 (左偏树) ≥ 0 右旋
> 1 (左偏树) < 0 先左旋后右旋
< -1 (右偏树) ≤ 0 左旋
< -1 (右偏树) > 0 先右旋后左旋

为便于使用,我们将旋转操作封装成一个函数。有了这个函数,我们就能对各种失衡情况进行旋转,使失衡节点重新恢复平衡。

/*执行旋转操作,使该子树重新恢复平衡*/
TreeNode *AVLTree::rotate(TreeNode *node){
	// 获取节点 node 的平衡因子
	int _balanceFactor = balanceFactor(node);
	// 左偏树
	if (_balanceFactor > 1){
		if (balanceFactor(node->left) < 0){
			// 先左旋child再右旋node
			node->left = leftRotate(node->left);
			return rightRotate(node);
		}
		else{
			// 直接右旋
			return rightRotate(node);
		}
	}
	// 右偏树
	if (_balanceFactor < -1){
		if (balanceFactor(node->right) > 0){
			// 先右旋child 再左旋node
			node->right = rightRotate(node->right);
			return leftRotate(node);
		}
		else
		{
			// 直接左旋
			return leftRotate(node);
		}
	}
	// 平衡树,无需旋转,直接返回
	return node;
}

AVL树常用操作

插入节点

AVL树的节点插入操作与二叉搜索树在主体上类似。唯一的区别在于,在AVL树中插入节点后,从该节点到根节点的路径上可能会出现一系列失衡节点。因此,我们需要从这个节点开始,自底向上执行旋转操作,使其所有失衡节点恢复平衡

/*递归插入节点(辅助方法)*/
TreeNode *AVLTree::insertHelper(TreeNode *node, int val){
	if (node == nullptr){
		return new TreeNode(val);
	}
	if (node->val < val){
		node->right = insertHelper(node->right, val);
	}
	else if (node->val > val){
		node->left = insertHelper(node->left, val);
	}
	else
		// 重复节点 直接返回
		return node;
	updateHeight(node); // 更新节点高度
	// 执行旋转操作,使该子树重新恢复平衡
	node = rotate(node);
	// 返回子树根节点
	return node;
}

删除节点

类似地,在二叉搜索树的删除节点方法的基础上,需要从底至顶执行旋转操作,使所有失衡节点恢复平衡。

/*删除节点*/
void AVLTree::remove(int val){
	root = removeHelper(root, val);
}
/*递归删除节点(辅助方法)*/
TreeNode *AVLTree::removeHelper(TreeNode *node, int val){
	if (node == nullptr){
		return nullptr;
	}
	/*查找节点并删除*/
	if (node->val < val){
		node->right = removeHelper(node->right, val);
	}
	else if (node->val > val){
		node->left = removeHelper(node->left, val);
	}
	else{
		// 子节点数量为0 或 1
		if (node->left == nullptr || node->right == nullptr){
			TreeNode *child = node->left == nullptr ? node->right : node->left;
			delete node;
			node = child;
		}
		// 子节点数量为 2
		else{
			// 找到中序遍历的后一个节点(右子树的最左边元素)
			TreeNode *tmp = node->right;
			while (tmp->left != nullptr){
				tmp = tmp->left;
			}
			int tmpVal = tmp->val;
			// 递归删除 (返回值是根节点)(删除右子树的最左边元素返回值就是右子树根节点)
			node->right = removeHelper(node->right, tmpVal);
			// 再覆盖值(相当于交换后删除)
			node->val = tmpVal;
		}
	}
	updateHeight(node); // 更新每次遇到的节点高度
	// 执行旋转操作,使该子树重新恢复平衡
	node = rotate(node);
	// 返回子树的根节点
	return node;
}

查找节点

AVL树的节点查找操作与二叉搜索树一致。

AVL树典型应用

  • 组织和存储大型数据,适用于高频查找、低频增删的场景。
  • 用于构建数据库中的索引系统。
  • 红黑树也是一种常见的平衡二叉搜索树。相较于AVL树,红黑树的平衡条件更加宽松,插入与删除节点所需的旋转操作更少,节点增删操作的平均效率更高。

标签:node,右旋,right,TreeNode,二叉,AVL,child,数据结构,节点
From: https://www.cnblogs.com/1873cy/p/18395797

相关文章

  • 数据结构和算法
    数据结构和算法数据结构数组(Array):一种线性数据结构,可以存储相同类型的元素,支持基于索引的快速访问。链表(LinkedList):由一系列节点组成,每个节点包含数据部分和指向下一个节点的指针。栈(Stack):遵循后进先出(LIFO)原则的线性数据结构,只能在一端(栈顶)进行添加或删除操作。队列(Queue):......
  • PART1-Oracle关系数据结构-分区、视图以及其他的对象
    4.分区、视图与其他对象4.1.分区概述分区允许您将非常大的表和索引分解成更小、更易于管理的部分,称为分区。每个分区是一个独立的对象,有自己的名称,并且可以选择拥有自己的存储特性。为了说明分区的概念,假设一个人力资源经理有一个大盒子,里面装着员工文件夹。每个文件夹都列出......
  • 【数据结构和算法实践-树-LeetCode100-判断是否是相同的树】
    数据结构和算法实践-树-LeetCode100-判断是否是相同的树题目MyThought代码示例JAVA-8题目给你两棵二叉树的根节点p和q,编写一个函数来检验这两棵树是否相同。如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。示例输入:p=[1,2,3],q=[1,2......
  • 【数据结构和算法实践-链表-LeetCode23-合并K个有序数组】
    合并K个有序数组题目MyThought代码示例JAVA-8题目合并K个有序数组MyThought一、将ListNode放入PriorityQueue中1.1、设置PriorityQueue的比较器规则1.2、将ListNode[]放入priorityQueue二、再将数据依次弹出放到ListNode中代码示例JAVA-8publicListNod......
  • Python 默认列表(Default List):一种灵活的数据结构
    Python中的默认列表(DefaultList)是一种特殊的数据结构,它允许我们创建一个包含特定元素类型的列表,并在需要时动态地添加或删除元素。这种灵活性使得默认列表成为了处理一些不确定或变化的数据的有力工具。创建列表时指定元素类型在Python中,我们可以在创建列表时指定元素类型,如果......
  • C++ 数据结构——二叉树(最最最最最实用的二叉树教程)
    本文章以实用为主,所以不多废话直接开整本文所介绍的二叉树是最基础的二叉树,不是二叉搜索树,也不是平衡二叉树,就基本的二叉树若需要Python版,请跳转到 Python数据结构——二叉树(最最最最最实用的二叉树教程)二叉树的构建二叉树为一个父节点连接到两个子节点,若还要加入新的......
  • 力扣-968监控二叉树(Java贪心详细题解)
    题目链接:968.监控二叉树-力扣(LeetCode)前情提要:本题是一道名副其实的hard题目,他考察二叉树和贪心的综合运用能力。所以我们不仅要会贪心还要会二叉树的一些知识,如果没有写二叉树类型的题目,建议大家该题可以放放,去刷其他的题目。因为本人最近都来刷贪心类的题目所以该......
  • 数据结构基本概念
    组织存储数据--->内存程序=数据结构+算法定义:一组用来保存一种或者多种特定关系的数据的集合(组织和存储数据)程序的设计:将现实中大量而复杂的问题以特定的数据类型和特定的存储结构存储在内存中,并在此基础上实现某个特定的功能的操作数据与数据之间的关系:数据的逻辑结......
  • Redis数据结构:Zset类型全面解析
    Redis数据结构:Zset类型全面解析Redis,作为一种高性能的键值对数据库,因其丰富的数据类型和高效的性能而受到了广泛的关注和使用。在Redis的五种主要数据类型中,Zset(有序集合)类型可能是最复杂,但也是最强大的一种。Zset不仅可以存储键值对,还可以为每个元素分配一个分数,然后根......
  • AVL树插入新节点操作的实现
    目录二叉搜索树的概念AVL树的概念AVL树节点的实现二叉搜索树插入操作平衡因子的更新旋转操作左单旋​编辑右单旋左右双旋右左双旋插入操作中补充旋转二叉搜索树的概念AVL树的概念一颗AVL树或是空树,或者是具有以下性质的二叉搜索树:它的左右子树都是AVL树左右......