AVL树(查找、插入、删除)——C语言
AVL树
平衡二叉查找树(Self-balancing binary search tree)通常是指一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且任意节点的左右两个子树都是一棵平衡二叉树(即严格的平衡二叉查找树,“严格”二字体现在任意节点的左右子树高度差不超过1),平衡二叉树有多种实现方法(红黑树、AVL、替罪羊树、Treap、伸展树等)本篇随笔分析的AVL树(AVL树是根据它的发明者G. M. Adelson-Velskii和E. M. Landis命名的),是在二叉查找树的基础上一个优化版本(AVL树是严格的二叉查找树,而红黑树不是,红黑树是非严格的二叉查找树,红黑树有自己的一套规则来保证整棵树接近平衡)
AVL树的特点:
1.本身首先是一棵二叉查找树
AVL树的作用
AVL树解决了二叉查找树可能出现的极端情况,对于一般的二叉搜索树(Binary Search Tree),其期望高度(即为一棵平衡树时)为log2n,其各操作的时间复杂度(O(log2n))同时也由此而决定,但是在某些极端情况下
(如在插入的序列是有序的时),二叉搜索树将退化成近似链或链,此时,其操作的时间复杂度将退化成线性的,即O(n)。我们可以通过随机化建立二叉搜索树来尽量的避免这种情况,但是在进行了多次的操作之后,例如在在删除时,
我们总是选择将待删除节点的后继代替它本身,这样就会造成总是右边的节点数目减少,以至于树向左偏沉。这同时也会造成树的平衡性受到破坏,使得它的操作时间复杂度增加
例如下面这种情况:

AVL树的特性让二叉搜索树的节点实现平衡(balance):节点相对均匀分布,而不是偏向某一侧
AVL树节点的定义:
typedef struct AVLTreeNode
{
int data;
int height; //节点的高度
struct BSTreeNode *left;//左子树
struct BSTreeNode *right;//右子树 }AVLTree;
与一般的二叉查找树的节点相比多了一个参数,节点的高度(网上有些博客是把平衡因子加在了节点的定义里面,笔者不太建议这样做)
预备知识
为了读者能更好了理解AVL树的操作,在继续往下看之前需要搞清楚几个概念
高度、深度和平衡因子
(1)深度——从上往下数

(树的深度 = 叶子节点的深度)
(2)高度——从下往上数
关于高度,有的文章中将"空二叉树的高度定义为-1",本篇随笔采用维基百科上的定义:空二叉树的高度为0,因为高度是从下往上数,所以叶子节点的高度为1

(树的高度 = 根节点的高度)
(3)平衡因子


(平衡因子计算时左子树 - 右子树 和 右子树 - 左子树 都可以,因为判断树是否平衡的条件是:每个结点的左右子树的高度之差的绝对值不超过1,只不过判断失衡以后还要判断是哪一种失衡,这就需要根据情况来选择是左-右还是右-左了)
-----------------------------------------------------------------------------------------------------
1、查找节点
在 AVL树 中查找与在 二叉查找树 中查找完全一样,因为AVL树总是保持平衡的,树的结构不会由于查询而改变,这里就不再赘述了
实现代码:
/* 查找特定值 */
void SearchData(int targ, BSTree *nod)
{
if (nod != NULL)
{
if (nod->data == targ)
{
printf("查找值存在,值为%d\n", nod->data);
}
else if (nod->data > targ)
{
SearchData(targ, nod->left); //递归查找左子树
}
else if (nod->data < targ)
{
SearchData(targ, nod->right); //递归查找右子树
}
}
else if (nod == NULL)
{
printf("查找值不存在\n");
}
}
2、插入节点(递归实现)
先梳理一下步骤

先来实现搜索最低失衡节点,搜索最低失衡节点是从新插入的节点(也就是叶子节点)往上搜索(也可以说成从新增结点开始向根部回溯),搜索到的第一个平衡因子>1(|左子树高度-右子树高度|>1)的节点,作为最低失衡节点,因为是从新插入的节点往上搜索,二叉树的搜索是单向的(结构体成员中只有左右子树),单独使用一个函数来实现逆向搜索实现起来并不方便,这里就把搜索最低失衡节点的操作放到递归实现的插入操作中
这里没有像上一篇随笔:二叉查找树(查找、插入、删除)——C语言那样先手动输入的一个二叉平衡树(因为这里要考虑节点的高度,输入不太方便),干脆就从空二叉树开始插入
实现代码:
/* 获取节点高度,空树的高度为0 */
int GetNodeHeight(AVLTree *nod)
{
if (nod != NULL) //若不为空子树
{
if (nod->left == NULL && nod->right == NULL) //若为叶子节点
{
return ;
}
else if (GetNodeHeight(nod->right) > GetNodeHeight(nod->left)) //若右子树高度较高
{
return (nod->right)->height + ;
}
else //若左子树高度较高
{
return (nod->left)->height + ;
}
}
else //若为空子树
{
return ;
}
} /* 添加新节点(包含搜索最低失衡节点和调整树操作) */
AVLTree *AddNewNode(AVLTree *nod, int NewData)
{
AVLTree *p = NULL; if (nod == NULL)
{
if ((nod = (AVLTree *)malloc(sizeof(AVLTree))) == NULL) //创建新节点
{
printf("内存不足");
exit();
}
nod->data = NewData;
nod->left = NULL;
nod->right = NULL;
nod->height = GetNodeHeight(nod);
}
else if (NewData > nod->data)
{
nod->right = AddNewNode(nod->right, NewData);
nod->height = GetNodeHeight(nod); if (GetNodeHeight(nod->right) - GetNodeHeight(nod->left) > ) //右子树高度 - 左子树高度
{ } return nod;
}
else if (NewData < nod->data)
{
nod->left = AddNewNode(nod->left, NewData);
nod->height = GetNodeHeight(nod); if (GetNodeHeight(nod->left) - GetNodeHeight(nod->right) > ) //左子树高度 - 右子树高度
{ } return nod;
}
else if (NewData == nod->data)
{
printf("不允许插入重复值");
exit();
} return nod;
}
(若二叉树中只有根节点,那么这个根节点也是叶子节点)
在上面的代码中已经实现了插入新节点并且搜索最低失衡节点的功能,这里可以用前序遍历二叉树并打印节点高度来判断插入节点函数是否正确(上面预留的调整二叉树函数的位置)
遍历二叉树:
/* 前序遍历AVL树,并打印节点高度 */
void PreOrder_Traverse(AVLTree *nod)
{
if (nod != NULL)
{
printf("data = %d height = %d\n", nod->data, nod->height); PreOrder_Traverse(nod->left);
PreOrder_Traverse(nod->right);
}
}
测试插入函数(保证每次插入新节点后的二叉树都是二叉平衡树)

测试数据图解:

测试结果:

搞清楚了各个节点的高度,平衡因子的计算也比较方便了,下面就是AVL树的核心操作“旋转”,不同的失衡情况有不同的旋转方式,一共有四种节点失衡情况,如下图

不同失衡情况下的示例二叉树,如下图(读者可能会发现“最低失衡节点的左子树的左子树还有非空节点”这个判断依据,对第二组图适用,但对于第一组图不太合适)

或者是


(LL型和RR型的操作相对简单)
第一种:LL型
LL型失衡,调整二叉树需要两步
第一步:将失衡节点的左子树的右子树 变成 失衡节点的左子树
第二步:失衡节点 变成 失衡节点未发生操作前左子树的右子树
只看上面的叙述有点绕,下面为实现代码和图片示例
实现代码:
/* LL型旋转 */
AVLTree * LL_Rotation(AVLTree *nod)
{
AVLTree *temp;
temp = nod->left; //临时保存nod的左子树 nod->left = nod->left->right; //将失衡节点的左子树的右子树 变成 失衡节点的左子树
temp->right = nod; //失衡节点 变成 temp的右子树 nod->height = GetNodeHeight(nod); //更新节点高度
temp->height = GetNodeHeight(temp); return temp;
}
LL型旋转图解

GIF图:

(图片来源:http://www.sohu.com/a/270452030_478315)
LL型失衡测试:
测试数据:

测试结果:

第二种:RR型
RR型的操作和基本相同,只是方向相反,这里就不再赘述了
实现代码:
/* RR型旋转 */
AVLTree * RR_Rotation(AVLTree *nod)
{
AVLTree *temp;
temp = nod->right; //临时保存nod的右子树 nod->right = nod->right->left;
temp->left = nod; nod->height = GetNodeHeight(nod); //更新节点高度
temp->height = GetNodeHeight(temp); return temp;
}
第三种:LR型
LR型失衡的操作相比于LL型失衡操作相对要复杂一点,需要旋转两次才能恢复平衡
第一步:对失衡节点的左子树进行RR型旋转
第二步:对失衡节点进行LL型旋转
因为之前已经写好了LL型和RR型的旋转,这里直接用就可以了,实现代码如下
/* LR型旋转 */
AVLTree * LR_Rotation(AVLTree *nod)
{
nod->left = RR_Rotation(nod->left);
nod = LL_Rotation(nod); return nod;
}
LR型旋转图解:

测试数据:

第三种:RL型
和LR型的旋转基本相同,这里就不再赘述了,实现代码如下
/* RL型旋转 */
AVLTree * RL_Rotation(AVLTree *nod)
{
nod->right = LL_Rotation(nod->right);
nod = RR_Rotation(nod); return nod;
}
3、删除节点
删除节点比插入节点的操作还要稍微复杂一点,因为插入时,进行一次平衡处理(一次平衡处理可能包含多次旋转),整棵树都会处于平衡状态,而在删除时,需要进行多次平衡处理,才能保证树处于平衡状态
AVL树的删除操作前半部分和二叉查找树相同,只不过删除后要检查树是否失去平衡,如果失衡就需要重新调整平衡,并更新节点高度,总的来说可以分为如下几种情况

(1)删除叶子节点
情况一:删除节点后二叉树没有失去平衡

删除节点后树没有失去平衡,这种情况下只需要更新节点的高度
情况二:删除节点后二叉树失去平衡

上图的RE型失衡只有在删除操作时才可能出现(在插入时不可能出现),RE型失衡的旋转方式和RR型失衡的旋转方式一模一样
(虽然删除节点时遇到的失衡情况多了两种 LE和RE ,但是旋转的方式依旧是那四种(LL、RR、LR、RL))
实现代码:
/* 删除节点 */
AVLTree *DeletNode(AVLTree *nod, int DelData)
{
AVLTree *SNode = NULL; //后继节点
AVLTree *PSNode = NULL; //后继节点的父节点
AVLTree *temp = NULL; //临时保存待释放节点的子树,避免free后找不到左右子树 if (nod == NULL)
{
printf("删除节点不存在");
exit();
}
else if (DelData > nod->data)
{
nod->right = DeletNode(nod->right, DelData); if (GetNodeHeight(nod->left) - GetNodeHeight(nod->right) > )
{
temp = nod->left; if (GetNodeHeight(temp->left) >= GetNodeHeight(temp->right)) //LL型或LE型失衡、两种情况处理方式相同
{
nod = LL_Rotation(nod);
}
else //LR型失衡
{
nod = LR_Rotation(nod);
}
} nod->height = GetNodeHeight(nod); //更新节点高度
}
else if (DelData < nod->data)
{
nod->left = DeletNode(nod->left, DelData); if (GetNodeHeight(nod->right) - GetNodeHeight(nod->left) > )
{
temp = nod->right; if (GetNodeHeight(temp->right) >= GetNodeHeight(temp->left)) //RR或RE型失衡、两种情况处理方式相同
{
nod = RR_Rotation(nod);
}
else //RL型失衡
{
nod = RL_Rotation(nod);
}
} nod->height = GetNodeHeight(nod); //更新节点高度
}
else if (DelData == nod->data)
{
if (nod->right == NULL && nod->left == NULL) //若待删除节点为叶子节点
{
free(nod);
return NULL;
}
}
(2)删除带有一个子节点的节点

else if (DelData == nod->data)
{
if (nod->right == NULL && nod->left == NULL) //若待删除节点为叶子节点
{
free(nod);
return NULL;
}
else if (nod->right == NULL && nod->left != NULL) //若待删除节点只有左子树
{
temp = nod->left;
free(nod); return temp;
}
else if (nod->right != NULL && nod->left == NULL) //若待删除节点只有右子树
{
temp = nod->right;
free(nod); return temp;
}
}
(3)删除带有两个子节点的节点
删除带有两个子节点的节点时,需要找到待删除的节点的后继节点或者前驱节点(本篇随笔使用后继节点),具体方法在上一篇随笔已经列出“二叉查找树(查找、插入、删除)——C语言”,这里不再赘述
else //若待删除节点既有左子树也有右子树
{
SNode = SearchSuccessorNode(nod->right); //搜索后继节点
PSNode = SearchParentofSNode(nod->right, nod->right); //搜索后继节点的父节点 if (nod->right == SNode) //后继节点为待删除节点的右子树(后继节点有右子树和没有右子树的操作相同)
{
SNode->left = nod->left;
free(nod); return SNode;
}
else if (nod->right != SNode && SNode->right == NULL) //后继节点不为待删除节点的右子树,并且该后继节点没有右子树
{
SNode->left = nod->left;
SNode->right = nod->right;
PSNode->left = NULL;
free(nod); return SNode;
}
else if (nod->right != SNode && SNode->right != NULL) //后继节点不为待删除节点的右子树,并且该后继节点有右子树
{ PSNode->left = SNode->right; //后继节点的右子树作为后继节点父节点的左子树
SNode->left = nod->left;
SNode->right = nod->right;
free(nod); return SNode;
}
}
}
需要注意的是,删除节点时不会出现“后继节点不是删除节点的子节点,且后继节点有右子树”这种情况,如下图

上图的14节点已经失衡了,在插入的时候就会被调整,所以不会出现“后继节点不是删除节点的子节点,且后继节点有右子树”这种情况
(由于笔者能力有限,AVL树的删除操作分析的不是很清楚,若有疏漏,清指出)
AVL树(查找、插入、删除)——C语言的更多相关文章
- AVL树的插入删除查找算法实现和分析-1
至于什么是AVL树和AVL树的一些概念问题在这里就不多说了,下面是我写的代码,里面的注释非常详细地说明了实现的思想和方法. 因为在操作时真正需要的是子树高度的差,所以这里采用-1,0,1来表示左子树和 ...
- AVL 树的插入、删除、旋转归纳
参考链接: http://blog.csdn.net/gabriel1026/article/details/6311339 1126号注:先前有一个概念搞混了: 节点的深度 Depth 是指从根 ...
- AVL树的插入和删除
一.AVL 树 在计算机科学中,AVL树是最早被发明的自平衡二叉查找树.在AVL树中,任一节点对应的两棵子树的最大高度差为 1,因此它也被称为高度平衡树.查找.插入和删除在平均和最坏情况下的时间复杂度 ...
- AVL树的插入操作(旋转)图解
=================================================================== AVL树的概念 在说AVL树的概念之前,我们需要清楚 ...
- AVL树的插入与删除
AVL 树要在插入和删除结点后保持平衡,旋转操作必不可少.关键是理解什么时候应该左旋.右旋和双旋.在Youtube上看到一位老师的视频对这个概念讲解得非常清楚,再结合算法书和网络的博文,记录如下. 1 ...
- Avl树的基本操作(c语言实现)
#include<stdio.h> #include<stdlib.h> typedef struct AvlNode *Position; typedef struct Av ...
- 创建AVL树,插入,删除,输出Kth Min
https://github.com/TouwaErioH/subjects/tree/master/C%2B%2B/PA2 没有考虑重复键,可以在结构体内加一个int times. 没有考虑删除不存 ...
- 第七章 二叉搜索树 (d2)AVL树:插入
- AVL树(平衡二叉查找树)
首先要说AVL树,我们就必须先说二叉查找树,先介绍二叉查找树的一些特性,然后我们再来说平衡树的一些特性,结合这些特性,然后来介绍AVL树. 一.二叉查找树 1.二叉树查找树的相关特征定义 二叉树查找树 ...
- 红黑树(RB-tree)比AVL树的优势在哪?
1. 如果插入一个node引起了树的不平衡,AVL和RB-Tree都是最多只需要2次旋转操作,即两者都是O(1):但是在删除node引起树的不平衡时,最坏情况下,AVL需要维护从被删node到root ...
随机推荐
- Spring Boot微服务电商项目开发实战 --- 基础配置及搭建
根据SpringBoot实现分布式微服务项目近两年的开发经验,今天决定开始做SpringBoot实现分布式微服务项目的系列文章,帮助其他正在使用或计划使用SringBoot开发的小伙伴们.本次系列文章 ...
- Image Classification
.caret, .dropup > .btn > .caret { border-top-color: #000 !important; } .label { border: 1px so ...
- rabbitmq升级新版本后,需要新建用户。新版本默认禁止别的机器用guest用户访问。
rabbitmq升级新版本后,需要新建用户.新版本默认禁止别的机器用guest用户访问.
- Spring Cloud 之 Zuul基础.
一.概述 API 网关是一个更为智能的应用服务器,它的定义类似于面向对象设计模式中的 Facade 模式,它的存在就像是整个微服务架构系统的门面一样,所有的外部客户端访问都需要经过它来进行调度和过滤 ...
- ybc云计算思维
YBC的云计算思维 计算机基础 一 计算机由5大单元组成 输入单元(鼠标 键盘) 存储单元(硬盘 内存) 逻辑单元(CPU) 控制单元(主板) 输出单元(显示器 音响 打印机) CPU CPU主要 ...
- Dock学习(一):容器介绍
一.什么是容器 1.容器是一种轻量级.可移植.自包含的软件打包技术,使应用程序可以在几乎任何地方以相同的方式运行.开发人员在自己的笔记本上创建并测试好的容器,无需任何修改就能够在生产系统的虚拟机.或物 ...
- Excel催化剂开源第29波-在Winform上使用富文本编辑器控件
富文本编辑器,一般都是BS架构专利一般,好像百度有一个开源的比较出名,但无奈这些都只能用在JS上,在BS网页端开发上使用.像Winform开发的VSTO,只能羡慕的份.和一般Winform上用的Ric ...
- Python学习4——条件、循环及其他语句总结
多种语句 打印语句: 导入语句: 赋值语句: 代码块: 条件语句: 断言: 循环: 推导: pass.dal.exec和eval : 学习到的新函数:(以下函数的应用代码均在IDLE测试通过) ch ...
- python面向过程编程小程序 -ATM(里面用了终端打印)
06.09自我总结 1.文件摆放 ├── xxxx │ ├── run.py │ └── fil_mode.py │ └── data_time.py │ └── loading.py │ └── d ...
- Windows 设置自启动计划任务(非登录启动)
原因:服务器会不定期重启,且重启后无人看管,不会有人去登录系统.导致我们做的一些开机启动程序失效,进而系统瘫痪. 解决方法: 自己理解,想要达到目的有两种方式:系统服务 & 计划任务配置. 计 ...