算法与数据结构——AVL树(平衡二叉搜索树)
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树,红黑树的平衡条件更加宽松,插入与删除节点所需的旋转操作更少,节点增删操作的平均效率更高。
 
算法与数据结构——AVL树(平衡二叉搜索树)的更多相关文章
- 【数据结构与算法Python版学习笔记】树——平衡二叉搜索树(AVL树)
		
定义 能够在key插入时一直保持平衡的二叉查找树: AVL树 利用AVL树实现ADT Map, 基本上与BST的实现相同,不同之处仅在于二叉树的生成与维护过程 平衡因子 AVL树的实现中, 需要对每个 ...
 - 算法进阶面试题04——平衡二叉搜索树、AVL/红黑/SB树、删除和调整平衡的方法、输出大楼轮廓、累加和等于num的最长数组、滴滴Xor
		
接着第三课的内容和讲了第四课的部分内容 1.介绍二叉搜索树 在二叉树上,何为一个节点的后继节点? 何为搜索二叉树? 如何实现搜索二叉树的查找?插入?删除? 二叉树的概念上衍生出的. 任何一个节点,左比 ...
 - 二叉搜索树、AVL平衡二叉搜索树、红黑树、多路查找树
		
1.二叉搜索树 1.1定义 是一棵二叉树,每个节点一定大于等于其左子树中每一个节点,小于等于其右子树每一个节点 1.2插入节点 从根节点开始向下找到合适的位置插入成为叶子结点即可:在向下遍历时,如果要 ...
 - 看动画学算法之:平衡二叉搜索树AVL Tree
		
目录 简介 AVL的特性 AVL的构建 AVL的搜索 AVL的插入 AVL的删除 简介 平衡二叉搜索树是一种特殊的二叉搜索树.为什么会有平衡二叉搜索树呢? 考虑一下二叉搜索树的特殊情况,如果一个二叉搜 ...
 - Java实现平衡二叉搜索树(AVL树)
		
上一篇实现了二叉搜索树,本章对二叉搜索树进行改造使之成为平衡二叉搜索树(Balanced Binary Search Tree). 不平衡的二叉搜索树在极端情况下很容易退变成链表,与新增/删除/查找时 ...
 - 算法:非平衡二叉搜索树(UnBalanced Binary Search Tree)
		
背景 很多场景下都需要将元素存储到已排序的集合中.用数组来存储,搜索效率非常高: O(log n),但是插入效率比较低:O(n).用链表来存储,插入效率和搜索效率都比较低:O(n).如何能提供插入和搜 ...
 - 再回首数据结构—AVL树(二)
		
前面主要介绍了AVL的基本概念与结构,下面开始详细介绍AVL的实现细节: AVL树实现的关键点 AVL树与二叉搜索树结构类似,但又有些细微的区别,从上面AVL树的介绍我们知道它需要维护其左右节点平衡, ...
 - 手写AVL平衡二叉搜索树
		
手写AVL平衡二叉搜索树 二叉搜索树的局限性 先说一下什么是二叉搜索树,二叉树每个节点只有两个节点,二叉搜索树的每个左子节点的值小于其父节点的值,每个右子节点的值大于其左子节点的值.如下图: 二叉搜索 ...
 - 自己动手实现java数据结构(六)二叉搜索树
		
1.二叉搜索树介绍 前面我们已经介绍过了向量和链表.有序向量可以以二分查找的方式高效的查找特定元素,而缺点是插入删除的效率较低(需要整体移动内部元素):链表的优点在于插入,删除元素时效率较高,但由于不 ...
 - LeetCode 将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树
		
第108题 将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树. 本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1. 示例: 给定有序数组: [-10 ...
 
随机推荐
- oeasy教您玩转vim - 88 - # 自动命令autocmd
			
 自动命令 autocommand 回忆 上次我们研究的是外部命令grep 可以在vim中使用grep 搜索的结果进入了列表 可以打开.遍历.跳转.关闭这个列表 也可以给列表中的匹配行或者每个文件执 ...
 - 「比赛记录」CF Round 954 (Div. 3)
			
Codeforces Round 954 (Div. 3) 题目列表: A. X Axis B. Matrix Stabilization C. Update Queries D. Mathemati ...
 - CF369D Valera and Fools 题解
			
题目链接 Luogu Codeforces 题意简述 有 \(n\) 个人站成一排,每人手中有 \(k\) 发子弹,每次每人会向除自己外编号最小的人开枪,第 \(i\) 个人开枪的命中率为 \(p_i ...
 - 01-初识springboot
			
目录 01,什么是springboot 02,如何使用springboot 01,什么是springboot springboot是一个基于spring框架开发出来的一个新的框架,目的是为了简化spr ...
 - Django 安全之跨站点请求伪造(CSRF)保护
			
Django 安全之跨站点请求伪造(CSRF)保护 by:授客 QQ:1033553122 测试环境 Win7 Django 1.11 跨站点请求伪造(CSRF)保护 中间件配置 默认的CSRF中 ...
 - 对比python学julia(第三章:游戏编程)--(第二节)公主迎圣诞(4)
			
4. 碰撞检测 .得分及生命 在第 4 个阶段,利用GameZero的碰撞检测功能,使公主角色能够接到雪花 .礼物或剪刀. 在"sdgz"项目目录中 ,把 version3.jl ...
 - 8、SpringMVC之RESTful案例
			
阅读本文前,需要先阅读SpringMVC之RESTful概述 8.1.前期工作 8.1.1.创建实体类Employee package org.rain.pojo; import java.io.Se ...
 - AI生成的图片是否具有版权:如何认定美术作品的“抄袭”行为?
			
相关: 实务丨如何认定美术作品的"抄袭"行为? 首先,我认为AI生成的图片是否具有版权这个问题就不是一个问题,或者说这不是一个正确的提法,应该说AI生成的某张图片是否具有版权?也可 ...
 - 国产操作系统   deepin  ——   UOS 系统下使用蓝牙音箱或蓝牙耳机不能正常工作
			
近日搞来了一个国产CPU的电脑,自带系统为UOS,具体可见: https://www.cnblogs.com/devilmaycry812839668/p/14828130.html 忽然发现这个系统 ...
 - 乌克兰学者的学术图谱case1
			
0. 人物:米哈伊洛·兹古罗夫斯基Mykhailo Zakharovych Zghurovskyi,也拼写为Mykhailo Zgurovsky,(乌克兰语:Михайло Захарович Згу ...