AVL树的原理及简单实现

AVL是带有平衡条件的二叉查找树。AVL树是每个节点的左子树和右子树的高度差最多为1的二叉查找树(空树的高度为-1,空树高度的作用后面会有解释,请务必记住AVL树的定义,该定义贯穿了数据结构实现的全部)。除了插入和删除外,所有树的操作都可以以时间O(logN)O(logN)O(logN)的时间完成。当进行插入和删除操作时,会破坏树的平衡关系,于是需要实时更新路径上节点的所有平衡信息并对树的平衡关系进行修正。而树的修正方式称为旋转:

旋转最基本的旋转方式为左旋和右旋:树的其他复杂的旋转方式都由这两种方式实现(如Spary树和红黑树的的旋转)。

旋转:

  • 左旋:对左儿子进行旋转。
  • 右旋:对右儿子进行旋转

AVL树的失衡种类及修正方法:

  • 左左:左旋:图中,k1节点的左节点比右节点高1保持平衡,k2节点失衡,因为k2左节点比右儿子节点高2

  • 左右:左儿子右旋,自己再左旋:图中,k2节点保持平衡,k1节点虽然右子树比左子树高1,但是仍然是平衡的。对于k3,它的左儿子节点比右儿子节点高2,不平衡。

  • 右右:右旋:高度关系同左左
  • 右左:右儿子左旋,自己再右旋:高度关系同左右

注意:对于以上的四种旋转方式,都可以看作某一个节点的子树在进行平衡,也就是说,上面四个图左边的树,都是某一棵树的子树。而这颗子树在进行平衡的时候,子树的根节点发生了变化,此时需要在子树发生旋转的同时,保证子树父节点的指向也随之改变。对于C++,可以采用引用的方式,将父节点的指针引用进来,改变函数参数的同时也改变了指向,但是此处是采用C语言实现,于是只能采用返回指针值得形式来修改,于是插入删除的函数形式都变成了Node * function(ElemType elem,Node* tree);而调用方式也都变成了pointer = function(elem,pointer)注意,这两个pointer是同一个变量。

这里需要提一下函数形参:函数参数无论是指针还是非指针,作为函数参数的时候(非引用方式)都是在内存中复制一份用于函数内部的计算。因此传值形式,改变值,不会对源值产生影响。而对于传指针的形式之所以能够改变值,是因为指针复制一份之后,虽然存储的位置发生了变化,但是指向的地址还是没有发生变化,因此可以通过*pointer的形式改变源值。但是,这不意味着改变函数参数中指针本身的值,就能够一起改变指针的源值,因为其本质上只是复制一份地址。

正是因为改变函数参数的指针值不能够改变源值,所以才需要返回指针并赋给源值。用C++的引用会方便很多。

AVL树插入维护采用自底向上的方式:对于除插入节点以外的路径上的节点,对其左右儿子的高度进行判断,如果不满足平衡条件,再分别读取左右儿子的儿子节点高度来确定需要旋转的类型。

插入函数:

Node * Insert(Elem_Type value,Node * Tree);

输入参数:节点数据,树地址

返回参数:树地址

使用方式:tree = Insert(value,tree);

插入逻辑:

  • 树为空:申请空间初始化并存储信息和高度,返回存储地址
  • 树非空:
    • 若插入点值等于当前节点值:

      • 基于不同的方式进行处理,比如在节点里加入一个count记录相同值的个数
    • 若插入点值小于当前节点值:
      • left_son=Insert(value,left_son);
    • 若插入节点大于当前值:
      • right_son=Insert(value,right_son);
    • 判断节点是否平衡:左右儿子之间的差值是否等于2,再确定旋转类型,并重新平衡AVL树
    • 更新节点高度

定义空树高度为-1的意义在于:因为插入的节点不需要判断是否平衡(肯定平衡),其父节点需要读取其左右儿子节点的高度值来判断是否平衡,当插入节点没有兄弟节点时,插入节点的父节点肯定有一个儿子节点指空,定义空树的高度有益于消除特判。这种情况下,插入节点父节点的儿子高度分别为0和-1,满足条件。如果插入节点引起了AVL树的不平衡,其祖父节点也会查询插入节点的兄弟节点,也解决了兄弟节点为空的特判。

AVL树删除维护

删除函数:Node * Delete(Elem_Type value,Node * Tree);

输入参数:待删除的节点值,树地址

输出参数:树地址

使用方式:tree = Delete(value,tree);

删除逻辑:

  • 当节点非空时

    • value小于当前值:tree->left = Delete(value,tree);
    • value大于当前值时:tree->right = Delete(value,tree);
    • value等于当前值时:
      • 当前节点左右儿子均不为空:从左子树找到一个数值最大的节点,令当前节点的值为数值最大节点的值(max_point = get_max();tree->value = max_point->value),并从左子树中删除值左子树最大节点(tree->left = Delete(max_point->value,tree->left)
      • 存在一个儿子节点为空,删除该节点,并返回一个儿子节点的地址
      • 左右儿子为空,删除该节点,返回NULL
    • 当节点非空(删除一个元素之后,节点可能为空,也有可能不为空),左右儿子可能失衡:
      • 判断左右儿子的高度关系,通过旋转平衡节点。
      • 更新节点高度。(可能会有这样的疑问,明明在旋转的时候更新了节点的高度,这里为什么还要写一次:对于需要旋转的,不会产生影响。但是不需要进行旋转的节点通过这个函数更新高度。)
  • 返回树地址

关于插入和删除的细节以及一些可能产生疑问的地方,我以注释的形式写在了下面的代码中。

#include<bits/stdc++.h>
using namespace std;
//AVL treestruct node{int val;node * left;node * right;int height;//根据AVL树的定义:树的左右节点高度只差不能大于1,因此需要保存高度信息
}; int height(node * tree){if(tree==NULL)return -1;else return tree->height;/*有的朋友也许会产生疑问:为什么要定义这样一个函数来判断高度,不是直接能通过指针读取吗?也许,有的朋友会这样问:采用Height函数是用于解决哪些情况下空指针的特判?这里我简单说一下:1.某节点没有子树(这个节点在插入的时候的高度是直接赋0的),此时在其左儿子插入一个值,当延插入路径回溯的时候,需要更新该节点的高度,但是此时它有一个节点为NULL,不能够通过node->right->height的方式得到高度(会报错,程序GG),此时可以特判,但是由于它是对称的,于是当插入的是右节点也需要判断一次。通过一个函数的形式,简化了判断。2.某个节点发生了失衡,假设是左边子树高度-右边子树高度==2,此时需要访问左子树的左右儿子节点高度关系来判断是左左型还是左右型,此时和1里面说的一样,当左子树存在一个节点为空,就需要特判。而右子树比左子树高且失衡时也是如此。特判过程过于繁琐,且容易出错。通过一个函数的形式,既简化了代码表示,又减少了失误率。*/
}node * makeempty(node * tree){if(tree!=NULL){makeempty(tree->left);makeempty(tree->right);free(tree); }return NULL;//在宏里可能有作用,但是在这份代码里一点作用都没有
}node * find(int val, node *tree){if(tree!=NULL){if(tree->val>val)return find(val,tree->left);else if(tree->val<val)return find(val,tree->right);}return tree;//普通的排序树查找
}node * getmax(node * tree){if(tree!=NULL){while(tree->right!=NULL)tree=tree->right;}return tree;
}node * getmin(node * tree){/*get_min和get_max函数关于NULL的特判需要解释一下:该函数不仅会在Delete函数中调用,也有可能单独调用,当单独调用的时候,需要判断是否为空当get_max在Delete函数中的调用,实际上是不需要判断为空的,因为其父节点左右子树非空是调用该函数的前提*/if(tree!=NULL){while(tree->left!=NULL)tree=tree->left;}return tree;
}node * left_son_rotate(node * tree){//左子树旋转node * k2 = tree;node * k1 = tree->left;k2 ->left = k1->right;k1->right = k2;//先更新k2 再更新k1 因为此时k2成为了k1的子树 自下往上更新高度k2->height = max(height(k2->left),height(k2->right))+1;k1->height = max(height(k1->left),height(k1->right))+1; return k1;//返回指针是为了改变源指针的值
}node * right_son_rotate(node * tree){//同上node * k2 =tree;node * k1 = tree->right;k2 ->right = k1->left;k1->left = k2;k2->height = max(height(k2->left),height(k2->right))+1;k1->height = max(height(k1->left),height(k1->right))+1; return k1;
}node * left_right_son_rotate(node * tree){//先旋转子树 再旋转自己tree->left = right_son_rotate(tree->left);return left_son_rotate(tree);
}node * right_left_son_rotate(node * tree){tree->right = left_son_rotate(tree->right);return right_son_rotate(tree);
}node * insert(int val,node *tree){if(tree==NULL){//到达叶子节点,此时申请空间,初始化并赋值,返回节点地址用于父节点更新儿子指针//的指向tree=(node*)malloc(sizeof(node));tree->val = val;tree->left=NULL;tree->right = NULL;tree->height =0;}else if(tree->val >val){//插入和删除均采用自底向上的方式,先执行插入/删除的操作,然后再平衡树//和更新节点高度tree->left = insert(val,tree->left);//所有的插入都会发生在叶子节点上 可以想想到达叶子节点的实现情况if(height(tree->left)-height(tree->right)==2){//该节点在插入操作之后失衡,由于是插入左子树,失衡只能是左子树比右子树高//判断是左左型还是左右型 根据类型调用相应的旋转函数if(val<tree->left->val)tree = left_son_rotate(tree);else tree = left_right_son_rotate(tree);}}else if(tree->val<val){//同leftinserttree->right = insert(val,tree->right);if(height(tree->right)-height(tree->left)==2){if(val>tree->right->val)tree = right_son_rotate(tree);else tree= right_left_son_rotate(tree); }}//此处没有写等于的情况,可以说这是实现了一个set,但是如果需要实现重复元素存储,可以在树节点中//加入一个count标记用来标记相同数值的节点个数,当运行到此处的时候,只需要执行count++即可tree->height = max(height(tree->left),height(tree->right))+1;/*这个高度更新需要解释一下:这里容易发生疑问的就是,明明上面在进行旋转操作的时候,就已经更新了节点的height信息,为什么这里还需要多此一举来再更新一次高度信息呢?其实不然,产生这种想法是把自己绕进去了,其实插入不一定会产失衡,比如说插入前左子树比右子树高1,插入完成后,左右子树一样高,此时就不会进入旋转操作,但是这个例子不更新节点高度不会产生影响,但是对于下面这个例子则不一样:插入前树的两个儿子节点一样高,插入后,两个儿子节点高度差为1,此时该节点的高度也会增加1,如果没有进行执行该代码,AVL树会产生bug*/return tree;
}node * deletenode(int val,node * tree){if(tree!=NULL){//显然,节点为空就说明树里面没有这个值(逐层递归到NULL了还没找到,就是没有),//返回NULLif(tree->val>val)tree->left = deletenode(val,tree->left);//操作逻辑同插入:先删除,再维护else if(tree->val<val)tree->right = deletenode(val,tree->right);else{//对于存储多个相同值的情况,可以这样判断,当count>1时执行--,当count=1时执行下面的代码if(tree->left!=NULL&&tree->right!=NULL){//左右节点均不为空,那么就找到左子树的最大节点,将其值存储在当前节点,并从左子树中删去//左子树的最大节点,替罪羊node * p =getmax(tree->left);tree->val = p->val;tree->left = deletenode(tree->val,tree->left);}else {//对于上面的那种情况,最终也会转化成这种情况,因为一棵树的最大节点必没有右子树,但//是可能有左子树,此时不满足上面if的条件,便转而执行该段代码node * p = tree;//保存当前节点的地址,当树结构修改完成之后,free该地址if(tree->left==NULL)tree = tree->right;//左儿子为空 返回右儿子(右儿子也可能为空)else if(tree->right == NULL)tree = tree->left;//否则返回左儿子 此时右儿子必为空,因为不为空不会进入该代码块,而是if后面的代码块free(p);//树结构更新完毕,删除该节点}}//当节点删除完毕之后,对树的结构进行修正,此时树的左右儿子均是平衡的。可以由下往上推理,对//于已经删除的节点,其父节点通过下面的代码进行了平衡,于是返回到其祖父节点,而祖父节点的儿//子节点,也就是删除节点的父节点是平衡的,于是逐层向上,满足左右儿子都是平衡的前提。if(tree!=NULL){//这里的tree判空容易忘记导致出错,这里必须要考虑到树可能为空,也就是待删除的节点既有没左儿//子又没有右儿子,虽然关于旋转判断的代码不会出错,但是高度更新的tree->height会发生访问错误if(height(tree->left)-height(tree->right)==2){if(height(tree->left->left)>height(tree->left->right))tree = left_son_rotate(tree);else tree = left_right_son_rotate(tree);}else if(height(tree->right)-height(tree->left)==2){if(height(tree->right->right)>height(tree->right->left))tree = right_son_rotate(tree);else tree = right_left_son_rotate(tree);}//这里的高度更新和插入一样,在已经进行旋转操作的节点来说,是多此一举,但是对于没有发生//旋转操作的节点,却是必不可少的。tree->height = max(height(tree->left),height(tree->right))+1;}}return tree;
}void print_tree(node * tree){if(tree!=NULL){print_tree(tree->left);printf("%d ",tree->val);print_tree(tree->right);}
}int main(){node * tree = NULL;tree = makeempty(tree);int a[]={3,2,1,4,5,6,7,16,15,14,13,12,11,10,9,8};for(int i=0;i<16;++i)printf("tree:%d insert:%d\n",i,a[i]),tree = insert(a[i],tree),print_tree(tree),puts("");for(int i=0;i<16;++i)printf("tree:%d delete:%d\n",i,a[16-i-1]),tree = deletenode(a[i],tree),print_tree(tree),puts("");//此处采用的例子是《数据结构与算法分析C语言版第二版》中AVL树的例子,而本博客中的代码也均来自该//书,今后有机会会将书的电子版,题解,源码等上传到CSDN
}

温故而知新,第一次学AVL树的时候,只是把代码给背会了,但是没有完全理解其中的逻辑,对于其中很多细节性的问题完全解答不了,当第二次学的时候,想更加了解其中的逻辑,以及为什么要这么实现,更加注重对细节的理解,于是有了这篇博客。今后当第三次看AVL树的时候,能够更加精进,对它的理解到达一个新的层次。文章中有不足和错误之处在所难免,感谢各位的谅解。

AVL树简单实现及原理相关推荐

  1. 数据结构与算法——AVL树类的C++实现

    关于AVL树的简单介绍能够參考: 数据结构与算法--AVL树简单介绍 关于二叉搜索树(也称为二叉查找树)能够參考:数据结构与算法--二叉查找树类的C++实现 AVL-tree是一个"加上了额 ...

  2. 数据结构与算法--面试必问AVL树原理及实现

    数据结构与算法–AVL树原理及实现 AVL(Adelson-Velskii 和landis)树是带有平衡条件的二叉查找树,这个平衡条件必须容易实现,并且保证树的深度必须是O(logN).因此我们让一棵 ...

  3. 浅谈二叉查找树、AVL树、红黑树、B树、B+树的原理及应用

    一.二叉查找树 1.简介 二叉查找树也称为有序二叉查找树,满足二叉查找树的一般性质,是指一棵空树具有如下性质: 任意节点左子树不为空,则左子树的值均小于根节点的值. 任意节点右子树不为空,则右子树的值 ...

  4. [ 数据结构 - C++] AVL树原理及实现

    问题引入: 在上文我们提到了二叉搜索树在按值顺序插入时,会形成单边树,会大大降低二叉搜索树的性能.因此我们要将二叉搜索树进行平衡,采取适当的平衡措施,从而减低二叉搜索树的高度,使二叉搜索树达到一个接近 ...

  5. C++实践笔记(四)----AVL树的简单实现

    关于AVL树的分析,请见:数据结构与算法分析学习笔记(二)--AVL树的算法思路整理 这里给出AVL树的结构定义以及insert,remove和print三种操作的例程:   1 #include & ...

  6. AVL树---最简单的实现

    一.定义: 我们知道二叉查找树,有很强的排序.查找.删除.插入的能力,但是,随着插入和删除的次数变多,二叉查找树的工作的效率有可能的变得没那么好了,因为二叉查找树的工作效率依赖于树的高度,树越高效率越 ...

  7. 【从蛋壳到满天飞】JS 数据结构解析和算法实现-AVL树(一)

    前言 [从蛋壳到满天飞]JS 数据结构解析和算法实现,全部文章大概的内容如下: Arrays(数组).Stacks(栈).Queues(队列).LinkedList(链表).Recursion(递归思 ...

  8. 从AVL树的定义出发,一步步推导出旋转的方案。

    本文从AVL树的定义出发,一步步地推导出AVL树旋转的方案,这个推导是在已经清楚地知道AVL树的定义这个前提下进行的.文章注重思考的过程,并不会直接给出AVL树是怎样旋转的,用来提醒自己以后在学习的时 ...

  9. 数据结构与算法——二叉平衡树(AVL树)详解

    文章目录 AVL树概念 不平衡概况 四种平衡旋转方式 RR平衡旋转(左单旋转) LL平衡旋转(右单旋转) RL平衡旋转(先右后左双旋转) LR平衡旋转(先左后右单旋转) java代码实现 总结 AVL ...

  10. AVL树(二叉平衡树)详解与实现

    公众号文章链接 AVL树概念 前面学习二叉查找树和二叉树的各种遍历,但是其查找效率不稳定(斜树),而二叉平衡树的用途更多.查找相比稳定很多.(欢迎关注数据结构专栏) AVL树是带有平衡条件的二叉查找树 ...

最新文章

  1. P4735 最大异或和(可持久化trie树、求最大区间异或和)
  2. 人工智能写散文之躲进你的心里记录温暖的你
  3. samba linux文件服务器 changepassword + httpd 实现用户web自行修改密码
  4. (译)用多重赋值和元组解包提高python代码的可读性
  5. Java BIO、NIO、AIO 学习
  6. Tomcat启动报404(eclipse)
  7. Nginx+Tomcat搭建集群环境
  8. 发生无法识别的错误_车牌识别系统的核心部件抓拍摄像机怎么安装?
  9. python json.dumps参数_json.dumps参数之解
  10. 分享一个 pycharm 专业版的永久使用方法
  11. Serializer和ModelSerializer
  12. 分解质因数_java
  13. HDU 1880 魔咒词典(字符串hash)
  14. 硬盘损坏,怪我咯?3分钟拯救硬盘里的小姐姐!
  15. 阿里P8整理总结,入职大厂必备Java核心知识(附加面试题
  16. 实现企业微信引流的三大思路
  17. 特里回归战世界杯 英足总主席力挺 霍奇森已拒绝
  18. java虚拟机之gc
  19. 深入理解ElasticSearch(八)索引管理
  20. 亲测可用,利用Python实现自动抢课脚本

热门文章

  1. 市场下行手机市场成血海,为何荣耀逆流而上?
  2. phalcon mysql_phalcon mysql_phalcon数据库操作
  3. Zbar源码解析——zbar_oho_条形码阅读器|2021SC@SDUSC
  4. C措辞教程第一章: C措辞概论 (5)
  5. 心血管疾病:评估驾驶适应性(英国DVLA)
  6. 扫雷算法实现(简易版,只能在CMD指令中玩)
  7. android主题切换框架,Prism(棱镜)——一款优秀的Android 主题动态切换框架
  8. 最全面的Fiddler界面讲解#工作原理#菜单栏#工具栏#底部状态栏#底部自带命令行控制台#session栏#request栏和response栏
  9. 妙招!如何用Python巧妙的批量合并 Excel!
  10. Fuzzy Clustering详解