AVL 树(自平衡二叉搜索树) 介绍

news2025/1/20 22:01:57

AVL 树(自平衡二叉搜索树) 介绍

  1. 前言

在介绍二叉搜索树的章节中提到,二叉搜索树可能退化为线性链表,失去作为二叉树的各种优势。那么过程中需要维持二叉树的形式,同时左右子树的深度差异可控,如果能实现这两个条件,那么我们称这样的树为自平衡二叉搜索树,由于这类数最早由苏联数学家 Georgy Adelson Velsky 和Landis共同提出,又称之为AVL树。

  1. AVL树特征

AVL树的每个结点都需要维护平衡因子,平衡因子的取值范围为-1,0,1,如果所有结点的平衡因子的取值范围都落在-1,0,1范围内,就称这棵树为自平衡二叉搜索树(AVL树)。

观察一个具体例子,下面一棵树就属于一颗AVL 树,因为每个结点的平衡因子都落在-1,0,1范围之内。

在这里插入图片描述

下面这棵树就不属于AVL树,因为有的结点的平衡因子为2.

在这里插入图片描述

  1. AVL树旋转

AVL树可以在元素插入过程中进行旋转处理,确保所有节点的平衡因子落在合理范围之内,如果由于元素插入导致节点的平衡因子落在合理范围之外,那么就需要通过不同模式的旋转操作实现再平衡。值得一提的是,所有的旋转和再平衡操作都发生在递归插入完成之后,也就是从叶子节点开始,不断向上进行不同的调整。

3-1. 左旋操作

左旋操作的对象涉及到两个结点X和Y,当X结点的平衡因子的值超过限制范围的时候,而且右子树比左子树要“重”,这是后就需要把Y作为新的根节点,相当于对X施加左旋的基本操作,操作完成后,整个树满足AVL树的基本要求。

旋转前:

在这里插入图片描述

左旋后,Y将作为子树的新根节点,同时满足平衡因子满足AVL树的基本要求。

在这里插入图片描述

3-2 右旋操作

在这里插入图片描述

右旋操作与左旋操作相反,如上图所示,需要以对Y进行右旋操作,旋转操作之后,二叉树的平衡因子满足AVL树的基本条件和要求,重新回归到AVL树的属性。

3-3 先左旋后右旋

先左旋后右旋的操作会涉及到三个结点,结点Z,X和Y,操作需要自下而上,先对X-Y结点组合施加左旋操作,

操作前:

在这里插入图片描述

第一步,左旋操作,

在这里插入图片描述

第二步,右旋操作

在这里插入图片描述

操作完成后,二叉树重新取得平衡,回归AVL树的属性。

3-4 先右旋再左旋

先右旋再左旋也涉及到三个结点,操作顺序也是自下而上(利用递归回退前的信息属性进行操作),先对靠近底部的子树进行右旋操作,而后再进行左旋处理。

先右旋处理

在这里插入图片描述

后左旋处理

在这里插入图片描述

  1. AVL树的平衡因子和结点高度

平衡因子定义为左子树高度和右子树高度差,由于在旋转过程中需要对平衡因子进行维护和更新,所以需要动态求出每个结点的子树在插入或删除后,本身的平衡因子的变化。结点的平衡因子取决于左右子树的高度差,所以如果需要更新结点的平衡因子,那么首先首先求解本节点两个子树的高度。
b a l a n c e _ f a c t o r = h e i g h t ( l e f t _ s u b t r e e ) − h e i g h t ( r i g h t _ s u b t r e e ) ; balance\_factor=height(left\_subtree)-height(right\_subtree); balance_factor=height(left_subtree)height(right_subtree);

  1. AVL树的插入操作

AVL树的插入操作与普通二叉树插入操作基本相同,唯一不同点在于,插入完成后,需要对节点自底而上进行平衡因子的更新及相应的转动操作。这些反转操作必须在插入完成后才能进行,插入前无法进行更新,这就类似单个递归的后续操作,当递归完成后,然后再对相应的信息进行相关操作。

在进行插入操作之前,需要进行对相关的辅助函数进行编写。其中get_height(Node *node)函数返回节点当前的高度,如果节点没有左右子树,节点高度定义为1。

typedef struct Node
{
    int key;
    struct Node *lchild;
    struct Node *rchild;
    int height;
}Node;


int get_height(Node *node)
{
    if(node==NULL)
    {
        return 0;
    }
    else
    {
        return node->height;
    }
}

根据上面的公式,可以求出每个节点的平衡因子,get_balance_factor(Node *node)函数的作用为求出某个节点的平衡因子。

int get_balance_factor(Node *node)
{
    if(node==NULL)
    {
        return 0;
    }
    else
    {
        return (get_height(node->lchild) - get_height(node->rchild));
    }
}

如果要得到某个节点的后继节点,那么就需要使用函数find_successor(Node *node)进行求解,然后返回某个相关的节点。

Node *find_successor(Node *node)
{
    Node *p;

    if(node==NULL)
    {
        return NULL;
    }

    p=node->rchild;

    while(p && p->lchild)
    {
        p=p->lchild;
    }

    return p;
}

如果需要创建新的节点,那么就需要采用函数make_node,创建新的节点,然后返回创建的节点即可。

Node *make_node(int key)
{
    Node *node;

    node=(Node*)malloc(sizeof(Node));
    node->height=1;
    node->lchild=node->rchild=NULL;
    node->key=key;

    return node;
}

左旋操作

Node *left_rotation(Node *node)
{
  	Node *rc;
  	Node *new_node;

  	rc=node->rchild;
  	node->rchild=rc->lchild;
  	rc->lchild=node; //node =rc;
  	new_node=rc;

    // from the bottom to top
  node->height=max(get_height(node->lchild),get_height(node->rchild))+1;

  new_node->height = max(get_height(new_node->lchild), get_height(new_node->rchild)) + 1;

  return new_node;
}

右旋操作

Node *right_rotation(Node *node)
{
    Node *lc;
    Node *new_node;

    lc=node->lchild;
    node->lchild=lc->rchild;
    lc->rchild=node;
    new_node=lc;

    //from the bottom to top
    node->height = max(get_height(node->lchild), get_height(node->rchild)) + 1;

    new_node->height = max(get_height(new_node->lchild), get_height(new_node->rchild)) + 1;

    return new_node;
}

节点插入的函数分析,插入节点的函数当中,出现node->lchild=insert_node(node->lchild,key)表达是,这个表达式的作用是把前一个栈弹出的值赋给node->lchild即可,所以它不需要返回值,只需要赋值即可。

Node *insert_node(Node *node, int key)
{
    int bf; //balance factor;
    if(node==NULL)
    {
        return make_node(key); //create the new node
    }

    if(key < node->key)
    {
        node->lchild=insert_node(node->lchild,key);
    }
    else if(key > node->key)
    {
        node->rchild=insert_node(node->rchild,key);
    }
    else
    {
        return node; //termination condition had been reached;
    }

    //update the balance factor of each node and rebalance the AVL tree
    node->height=max(get_height(node->lchild),get_height(node->rchild))+1;
    bf=get_balance_factor(node);

    if(bf>1 && key < node->lchild->key)
    {
        return right_rotation(node);
    }

    if(bf<-1 && key >node->rchild->key)
    {
        return left_rotation(node);
    }

    if(bf>1 && key > node->lchild->key)
    {
        node->lchild=left_rotation(node->lchild);
        return right_rotation(node);
    }

    if(bf<-1 && key < node->rchild->key)
    {
        node->rchild=right_rotation(node->rchild);
        return left_rotation(node);
    }
    

    return node; //general root node;
}

节点删除函数分析,节点的删除分为三类情况:

如果节点的左子树或右子树为空,那么直接把其右子树或左子树赋给当前节点即可。如果左右子树均不为空,那么就需要找到此节点的后继节点,用后继节点的值替换待删除节点的值,最后把后继节点删除即可,后继节点可以为叶子节点也可以为中间的某个节点。

具体的实现函数为

Node *delete_node(Node *root, int key)
{
    //Deleting the node is the best algorithm we've ever had, it deserves learning
    
    Node* temp;
    int bf;
    
    
    if(root==NULL)
    {
        return root; //failure to find the target node
    }

    if(key < root->key)
    {
        root->lchild=delete_node(root->lchild,key);
    }
    else if(key > root->key)
    {
        root->rchild=delete_node(root->rchild,key);
    }
    else
    {
        if(root->lchild==NULL || root->rchild==NULL)
        {
            temp=(root->lchild?root->lchild:root->rchild);

            if(temp==NULL)
            {
                temp=root;
                root=NULL;
            }
            else
            {
                *root=*temp; //it will equal to left child or righ child;
            }

            free(temp);
            temp=NULL;
        }
        else
        {
            temp=find_successor(root);
            root->key=temp->key;

            //Highlight these application
            root->rchild=delete_node(root->rchild,temp->key); //delete the leaf key
        }
    }

    if(root==NULL) //key steps, if the last node is empty, then return empty
    {
        return root;
    }

    root->height=max(get_height(root->lchild),get_height(root->rchild))+1;

    bf=get_balance_factor(root);

    if(bf>1 &&get_balance_factor(root->lchild)>=0) //>=0;
    {
        return right_rotation(root);
    }

    if (bf > 1 && get_balance_factor(root->lchild) < 0)
    {
        root->lchild=left_rotation(root->lchild);
        return right_rotation(root);
    }


    if(bf<-1 && get_balance_factor(root->rchild)<=0)//<=0
    {
        return left_rotation(root);
    }

    if (bf < -1 && get_balance_factor(root->rchild) >0)
    {
        root->rchild=right_rotation(root->rchild);
        return left_rotation(root);
    }

    return root;
}
  1. 小结

要理解AVL算法,关键和核心是理解递归后的处理逻辑,递归后如果需要对子树进行旋转,那么旋转后直接返回即可,否则则需要原来的节点。这里面涉及到比较复杂的递归利用。

不同于严蔚敏《数据结构》,这里实现的算法比较直观,没有《数据结构》里面taller对树的高度的判断,如果有时间将分析严蔚敏版本的AVL树,里面的代码简洁,但是对于删除操作实现,难度很高。

参考资料

https://www.programiz.com/dsa/avl-tree

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/469114.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

要强的董经贵,与跳不出两轮电动车品质“陷阱”的雅迪

文|智能相对论 作者|陈明涛 “雅迪缺乏一个明确的战略定位&#xff0c;整个企业都没有方向&#xff0c;直接导致雅迪在前期竞争中失去先机。” “行业主要竞争对手更擅长销售和价格战&#xff0c;雅迪一直疲于应付、资源消耗大&#xff0c;结果也不理想。” “团队和经销商…

weblogic 反序列化 (CVE-2020-2551)漏洞复现(vulfocus)

漏洞简介 weblogic 反序列化 &#xff08;CVE-2020-2551&#xff09;漏洞是基于IIOP协议执行远程代码进行利用。 启动服务 http://192.168.5.128:10710/console/login/LoginForm.jsp 首先打开此连接&#xff0c;初始化weblogic服务。 准备工具 exp-自己写一个即可. javac…

企业编码生成系统--Python基础项目(4)

1. 成品展示&#x1f697;&#x1f680;&#x1f6eb; 运行&#x1f680;&#x1f680;&#x1f680;&#xff1a;1.在PyCharm中运行《企业编码生成系统》即可进入如图1所示的系统主界面。2.在该界面中可以选择要使用功能对应的菜单进行不同的操作。3.在选择功能菜单时&#x…

搭建electron-vue

electron-vue 准备工作修改package.jsonappveyor.yml.travis.yml.gitignore.eslintrc.js.eslintignore.babelrcsrc/renderer/main.jssrc/renderer/App.vuesrc/renderer/store/index.jssrc/renderer/store/modules/Counter.jssrc/renderer/store/modules/Counter.jssrc/renderer…

GT928 TP驱动跟读及虚拟按键上报解析

目前公司TP常用一套代码。MTK 平台使用.ko形式加载&#xff0c;所以跟读一下加深理解。 static struct i2c_driver tpd_i2c_driver {.driver {.of_match_table of_match_ptr(gt9xx_dt_match),},.probe tpd_i2c_probe,.remove tpd_i2c_remove,.detect tpd_i2c_detect,.dr…

万物皆数,算无止境 | 「雪浪算力开发者大赛」圆满收官

时在中春&#xff0c;阳和方起。 4月23日&#xff0c;「雪浪算力开发者大赛」 在雪浪小镇迎来完美收官。 本次大赛吸引了全球超过2422名开发者&#xff0c; 共有624支企业队伍&#xff0c; 288支高校队伍报名参赛&#xff0c; 累计提交作品9895份。 经过三个月激烈的角逐…

Linux shell编程 循环语句for continue break

for循环是编程语言中一种循环语句 示例1&#xff1a;循环读取user.txt中的用户名&#xff0c;创建用户。设置密码。 for i in $(cat /opt/user.txt) douseradd $iecho 123456 | passwd --stdin $i done 示例2&#xff1a;循环读取ipaddr文本文件中地址&#xff0c;执行ping命令…

基于空间矢量脉宽调制(SVPWM)的并网逆变器研究(Simulink)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

day38—选择题

文章目录 1.在计算机网络中&#xff0c;TCP和UDP协议的相似之处是&#xff08;D&#xff09;2.下列哪项最恰当地描述了建立TCP连接时“第一次握手”所做的工作&#xff08;C&#xff09;3.关于以下 URL 的描述错误的是&#xff08;A&#xff09;4.不属于交换机攻击的是&#xf…

LangChain入门指南

LangChain入门 什么是LangChain如何使用 LangChain&#xff1f;LangChain的模型LangChain 的主要特点使用示例构建语言模型应用程序&#xff1a;LLMPrompt Templates: 管理LLMs的Prompts构建语言模型应用程序&#xff1a;Chat Model完整代码 什么是LangChain LangChain是一个强…

【C语言】leetcode每日一题

目录 前言题目描述题目分析代码描述 前言 时间过得真快&#xff0c;马上又要回家了&#xff0c;马上又要开始卷了。不是每朵鲜花都能代表爱情&#xff0c;但是玫瑰做到了&#xff1b;不是每棵树都能耐得住干渴&#xff0c;但是白杨做到了&#xff1b;不是每个人都在追求上进&a…

【算法思维】-- 动态规划(C++)

OJ须知&#xff1a; 一般而言&#xff0c;OJ在1s内能接受的算法时间复杂度&#xff1a;10e8 ~ 10e9之间&#xff08;中值5*10e8&#xff09;。在竞赛中&#xff0c;一般认为计算机1秒能执行 5*10e8 次计算。 时间复杂度取值范围o(log2n)大的离谱O(n)10e8O(nlog(n))10e6O(nsqrt(…

从CI/CD持续集成部署到DevOps研发运维一体化

今天整理下从传统的CI/CD到DevOps研发运维一体化的整个演进过程。类似于每日构建和冒烟测试&#xff0c;实际上在10多年前就已经在实践&#xff0c;比如当前用的笔记多的AntCruiseControl方式来实现自动化的编译构建和持续集成能力。 包括当前DevOps过程实践中的持续集成&…

基于Springboot的班级综合测评管理系统的设计与实现

摘要 随着互联网技术的高速发展&#xff0c;人们生活的各方面都受到互联网技术的影响。现在人们可以通过互联网技术就能实现不出家门就可以通过网络进行系统管理&#xff0c;交易等&#xff0c;而且过程简单、快捷。同样的&#xff0c;在人们的工作生活中&#xff0c;也就需要…

Android内存泄漏问题排查分析及常见解决方案

什么是内存泄漏&#xff1a; 在Android开发过程中&#xff0c;当一个对象已经不需要再使用了&#xff0c;本该被回收时&#xff0c;而另个正在使用的对象持有它引用从而导致它不能被回收&#xff0c;这就导致本该被回收的对象不能被回收而停留在堆内存中&#xff0c;内存泄漏就…

你真的熟悉多线程的程序的编写?快来查漏补缺

目录 一、Thread 类的属性及常用的构造方法 1.1、 Thread 常见构造方法 1.2、Thread 类的常见属性 1.3、启动&#xff08;创建&#xff09;一个线程 1.4、中断一个线程 1.5、等待一个线程 1.6、休眠当前线程 1.7、当前线程让出的 CPU 资源 二、线程状态 一、Thread 类…

华为OD机试真题(Java),整数编码(100%通过+复盘思路)

一、题目描述 实现一个整数编码方法&#xff0c;使得待编码的数字越小&#xff0c;编码后所占用的字节数越小。 编码规则如下&#xff1a; 编码时7位一组&#xff0c;每个字节的低7位用于存储待编码数字的补码&#xff1b;字节的最高位表示后续是否还有字节&#xff0c;置1表…

2023联网公司时薪排行榜出炉,多多排榜首。微软、美团很强

今天分享一个对于选择公司非常有用的参考&#xff1a;“互联网时薪”。 我们在选择一个公司的时候&#xff0c;往往会比较关注总收入package (除了基本的月薪&#xff0c;加上其他的所有的收入&#xff0c;包括但不限于奖金、股票或股份的分红等等)。 然而&#xff0c;总收入…

算力网络安全

算力网络安全 1. 算力网络简介1.1 基本概念1.2 应用场景 2. 算力网络安全需求3. 算力网络安全架构3.1 算力网络参考架构3.2 资源层安全3.3 控制层和编排管理层安全3.4 服务层安全 4. 算力网络安全关键技术4.1 安全计算4.2 安全编排4.3 数据溯源4.4 可信内生4.5 操作审计4.6 安全…

【服务器】Linux搭建我的世界服务器 + 公网远程联机教程

Yan-英杰的主页 悟已往之不谏 知来者之可追 C程序员&#xff0c;2024届电子信息研究生 目录 前言 1. 安装JAVA 2. MCSManager安装 3.局域网访问MCSM 4.创建我的世界服务器 5.局域网联机测试 6.安装cpolar内网穿透 7. 配置公网访问地址 8.远程联机测试 9. 配置固定…