[数据结构] AVL树 模拟实现AVL树

标题:[数据结构] AVL树 && 模拟实现AVL树

@水墨不写bug



正文开始:

目录

(一)普通二叉搜索树的痛点 

 (二)AVL树简介

(1)AVL树的概念 

(三)AVL树的实现

(1)AVL树节点的定义

(2)AVL树的插入

(3)AVL树的旋转

i,左单旋(新节点插入较高右子树的右侧---右右:左单旋)

ii,右单旋(新节点插入较高左子树的左侧---左左:右单旋)

iii,左右双旋(新节点插入较高左子树的右侧---左右:先左单旋再右单旋)

iv,右左双旋(新节点插入较高右子树的左侧---右左:先右单旋再左单旋)

(四)AVL树的验证

(五)适用场景与性能分析


(一)普通二叉搜索树的痛点 

        在学习map和set之前,我们先认识一下AVL树和红黑树,他们是平衡二叉树,不同的是控制平衡的方法不同。本文主要讲解AVL树的概念以及实现的原理,从源代码角度带你理解AVL树的控制平衡的方法。

        map/multimap/set/multiset这几个容器有个共同点是:其底层都是按照二叉搜索树来实现的。
但是一般的二叉搜索树有一个致命的缺陷:往树中插入元素有序或者接近有序,二叉搜索树会退化为接近链表形状的单支树结构,时间复杂度会退化为O(N)。

        为了能够利用二叉树的结构优势,同时避免二叉树时间复杂度退化的缺陷,AVL树横空出世。


 (二)AVL树简介

        AVL树的概念由两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年提出,目的是为了解决在  数据有序或者接近有序的二叉搜索树  中查找元素时的  时间复杂度退化的问题。

        平衡的二叉搜索树结构查找的时间复杂度为O(logN),是一个高效的查找复杂度。但是一旦在构建二叉树时,数据有序或者接近有序,那么二叉搜索树会退化为单支树,这就相当于在链表中查找元素,复杂度为O(N),效率低下。

        两位数学家提出:当向二叉搜索树中插入新节点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
 

(1)AVL树的概念 

一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

  •         它的左右子树都是AVL树
  •         左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)

什么是平衡因子? 

        平衡因子是人为引入的快速判断二叉树是不是AVL树的一个标准。

        平衡因子是一个变量,它存储在每一个二叉树节点中。

        根据习惯,平衡因子=右子树的高度-左子树的高度。

(图中为平衡因子的计算结果)

        一旦一个节点的平衡因子的绝对值超过1,这表示这个根节点的左右子树的高度差超过1,那么就需要特殊操作(旋转),使得这棵树的左右子树的高度差的不超过1。

        如果一棵树是高度平衡的,那么它就是AVL树。如果他有n个节点,那么他的高度可以保持在log(n),那么在搜索时,时间复杂度就降下来了,为O(logN)。 


(三)AVL树的实现

        AVL树的特点是仅仅使用旋转这一种特殊操作来维持搜索树的平衡,这与其他的维持搜索树平衡的方法相比,是一个特点。本文我们依靠在节点中加入parent指针来更新平衡因子,来实现AVL树,但是这并不代表实现AVL树的方法仅此一种。

(1)AVL树节点的定义

        对于节点内存储的值,可以是一个值key,也可以是一个键值对pair{key,val},但是只存一个值key可以归为pair{key,key},存的值与键值相等,于是,本文的AVL树采用内部存储pair{key,val}键值对。

什么是key,val?

        key是存取时判断的标准,val是key对应的值。key,val都可以是int;也可以key是int,val是string。(需要根据实际情况判断)

什么是pair? 

        pair是一种STL中的类:

        该类将一对不同类型的值(T1和T2)耦合在一起。单个值可以通过它的公共成员first和second来访问。

AVL树节点的定义:

template<class K,class V>
struct AVLTreeNode
{AVLTreeNode(const pair<K,V>& pair_ = pair<K,V>()): _left(nullptr), _right(nullptr), _parent(nullptr)//根节点的_root是空, _pair(pair_), _bf(0){}AVLTreeNode<K, V>* _left;AVLTreeNode<K, V>* _right;AVLTreeNode<K, V>* _parent;pair<K, V> _pair;int _bf;   // 节点的平衡因子
};

        唯一需要注意的是缺省参数要给  pair<K,V>()  表示一个匿名对象,这种写法的好处我们以前文章讨论过,这里不再赘述。
 

(2)AVL树的插入

        AVL树的插入过程分为两个步骤:

                (1)与普通二叉树相同的插入操作;

                (2)调整节点的平衡因子,根据平衡因子,判断是否要进行旋转操作。

         就插入操作而言,AVL树相对于普通的二叉搜索树,对二叉树的结构(或者形状)更加关切。

插入操作:(普通二叉树)

        处理特殊情况,根为空,直接插入即可。

        对于一般情况,先根据键值key找到插入节点的位置,接下来进行的就是节点间指针的连接了——为了在找到插入位置时能够找到插入位置的_parent的位置,创建一个parent指针在cur向下查找的时候记录parent,这样就可以成功得到_parent。但是,现在子节点可以找到父亲,但是父亲没有办法确定子节点是他的左孩子还是右孩子,想要成功连接,还需要确定当前节点是parent的左还是右,所以需要有一次单独的判断。

(具体实现如下:)

// 在AVL树中插入值为data的节点
bool insert(const pair<K,V>& pair)//1,插入  2,调整平衡因子
{//处理特殊情况if (_root == nullptr){_root = new Node(pair);return true;}Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_pair.first > pair.first){parent = cur;cur = cur->_left;}else if (cur->_pair.first < pair.first){parent = cur;cur = cur->_right;}else//插入的节点等于data时退出{return false;}}//cur就是要插入的位置,parent记录父亲位置cur = new Node(pair);if (pair.first > parent->_pair.first){parent->_right = cur;}else{parent->_left = cur;}cur->_parent = parent;

插入操作:(AVL树)

两个步骤:

        (1)与普通二叉树相同的插入操作;(与上面完全一致)

        (2)调整节点的平衡因子,根据平衡因子,判断是否要进行旋转操作。(这是AVL树多做的部分)


如何调整平衡因子?

        平衡因子是每一个节点都有的,并且插入一个节点,只会对插入的子树的父祖节点的平衡因子产生影响。所以我们使用迭代法,从下向上依次更新平衡因子:

       

        每一次迭代后,cur和parent会向上移动,同时平衡因子也会更新。

更新平衡因子的方法是比较容易想的:

        只看当前新插入节点和parent节点——如果心插入节点是parent左,平衡因子_bf = 右子树高度 - 左子树高度;在左侧插入会导致平衡因子减小1;

        只看当前新插入节点和parent节点——如果心插入节点是parent右,平衡因子_bf = 右子树高度 - 左子树高度;在→侧插入会导致平衡因子增大1;

if (cur == parent->_left)parent->_bf--;
elseparent->_bf++;

(上述代码是在每次迭代后的更新平衡因子操作)

如何根据平衡因子来判断是否需要旋转?

        在更新平衡因子之后,需要根据平衡因子来判断是否需要旋转,更新平衡因子之后,父节点的平衡因子只有三种情况:

//parent的平衡因子为0    ——原来是1、-1,插入后为0,插入的是较矮的一边,高度不变化,不需要向上追踪父族
//parent平衡因子是-1、1  ——原来是0,插入后为1、-1,原来平衡,插入后不平衡,高度变化,需要向上
//parent平衡因子是2、-2  ——原来是1、-1,插入后加剧了不平衡,高度变化,需要旋转

 我们只需要将上述的三种情况转化为代码即可:

while (parent)//parent的平衡因子为0    ——原来是1、-1,插入后为0,插入的是较矮的一边,高度不变化,不需要向上追踪父族//parent平衡因子是-1、1  ——原来是0,插入后为1、-1,原来平衡,插入后不平衡,高度变化,需要向上//parent平衡因子是2、-2  ——原来是1、-1,插入后加剧了不平衡,高度变化,需要旋转
{if (cur == parent->_left)parent->_bf--;elseparent->_bf++;if (parent->_bf == 0){break;}else if (parent->_bf == 1 || parent->_bf == -1){cur = parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2)//需要旋转{//....}else{assert(false);}
}

        在向上迭代的过程中,如果cur节点的parent节点的平衡因子为2或者-2,这就表明这棵树已经不平衡了,将parent和cur所在的子树单独拿出来,旋转处理。


(3)AVL树的旋转

        AVL树的旋转总结来说一共右四种情况,分别是:左单旋,右单旋,左右双旋,右左双旋。

这四种情况的区别仅仅是插入节点的位置不同。 


        以下四种情况是触发旋转后才考虑的,如果在插入后没有触发旋转,则对下面四种旋转的讨论是没有意义的 ;(画的图也是刚好可以触发旋转的抽象图,目的是便于理解和梳理思路,便于写代码)

i,左单旋(新节点插入较高右子树的右侧---右右:左单旋)

        通过观察,我们发现:

        1,旋转的过程需要将bf等于2或者-2的子树拿出来进行——新节点的插入导致15的bf==2,于是把15这课子树拿出来进行旋转处理。

        2,旋转后,整棵树变化为平衡二叉树(左子树和右子树高度之差小于2)。

        3,至于旋转的为什么这样旋,以及可行性问题等,可以参考当年提出AVL树的学术论文。


        这里我们先考虑具体实现:需要几个指针变量记录节点的地址,便于改变树的形状:

parent为树的根节点,subR,subRL不再赘述。

对于一种特殊情况:当高度H==0,这时subRL==nullptr,不能再对subRL解引用,所以在访问subRL的时候,需要特殊判空。

void RotateL(Node* parent)
{Node* subR = parent->_right, * subRL = subR->_left;parent->_right = subRL;if (subRL)subRL->_parent = parent;subR->_left = parent;parent->_parent = subR;//....
}

        接下来,parent的父亲节点以及父亲节点和parent之间的关系没有确定,所以需要一个变量提前保存parent的父亲节点:

Node* Parentparent = parent->_parent;

        判断parent与他的父亲节点的关系:

        特殊的,如果parent的父亲是空,说明parent是整棵树的根,这种情况直接将旋转后的新根赋值给_root即可;

        一般情况,需要判断parent是他的父亲的左孩子还是右孩子:

if (Parentparent == nullptr)
{subR->_parent = nullptr;_root = subR;
}
else
{if (Parentparent->_left == parent){Parentparent->_left = subR;}else{Parentparent->_right = subR;}subR->_parent = Parentparent;
}

        最后,根据抽象图更新平衡因子即可:

			subR->_bf = parent->_bf = 0;

左单旋整体代码逻辑:

	void RotateL(Node* parent){Node* subR = parent->_right, * subRL = subR->_left;Node* Parentparent = parent->_parent;parent->_right = subRL;if (subRL)subRL->_parent = parent;subR->_left = parent;parent->_parent = subR;if (Parentparent == nullptr){subR->_parent = nullptr;_root = subR;}else{if (Parentparent->_left == parent){Parentparent->_left = subR;}else{Parentparent->_right = subR;}subR->_parent = Parentparent;}subR->_bf = parent->_bf = 0;}

ii,右单旋(新节点插入较高左子树的左侧---左左:右单旋)

        右单旋与与左单旋,无论是插入位置,触发条件,还是旋转处理,都与左单旋类似(某一种对称)这里给出右单旋抽象图,不再赘述:

实现参考:

// 右单旋
void RotateR(Node* parent)
{Node* subL = parent->_left, * subLR = subL->_right;Node* Parentparent = parent->_parent;parent->_left = subLR;if (subLR)subLR->_parent = parent;subL->_right = parent;parent->_parent = subL;if (Parentparent == nullptr){_root = subL;subL->_parent = nullptr;}else{if (Parentparent->_left == parent){Parentparent->_left = subL;}else{Parentparent->_right = subL;}subL->_parent = Parentparent;}//更新平衡因子,下方的树的平衡因子没有影响subL->_bf = parent->_bf = 0;
}

        通过总结,我们发现:插入在较高的子树的同侧(较高的左子树的左侧,较高的右子树的右侧),只需要进行一次旋转就可以树平衡化;但是对于插入位置在较高的子树的异侧时,就需要换一种处理方法——双旋。

iii,左右双旋(新节点插入较高左子树的右侧---左右:先左单旋再右单旋)

        对于这棵子树(一般来说是子树,也可能是一整颗树),90节点为根,30节点为左子树,它相对于90的右子树更高。

        插入位置在较高的左子树(30这棵子树)的右侧;这时需要先对30这棵子树左单旋,再对90这棵子树右单旋,最终90这棵子树成为平衡树。 

        

在具体实现中,我们可以复用已经实现的左单旋和右单旋的函数:

void RotateLR(Node* parent)
{Node* subL = parent->_left, * subLR = subL->_right;RotateL(subL);RotateR(parent);//.....
}

        但是需要注意,在旋转后,需要手动更新平衡因子,因为单旋中更新的平衡因子是只适合单旋的情况,对于双旋,自己设计更新平衡因子:

        通过观察抽象图,我们发现,左右双旋的插入位置无非只有两个:

这两种情况我们可以通过观察60的平衡因子来判断,于是,在旋转之前,提前保存60这个节点的平衡因子:

int bf = subLR->_bf;

        接下来,根据60节点的平衡因子的情况,更新90这棵子树中,插入位置的父祖节点的平衡因子即可。

        特殊处理:60这个节点的平衡因子可能为0,这表示60这个节点本身就是插入的新节点。依然根据抽象图分类假设,更新平衡因子即可。

左右双旋代码实现:

// 左右双旋
void RotateLR(Node* parent)
{Node* subL = parent->_left, * subLR = subL->_right;int bf = subLR->_bf;RotateL(subL);RotateR(parent);if (bf == 0){parent->_bf = 0;subL->_bf = 0;subLR->_bf = 0;}else if (bf == 1){subLR->_bf = 0;parent->_bf = 0;subL->_bf = -1;}else if (bf == -1){subLR->_bf = 0;parent->_bf = 1;subL->_bf = 0;}elseassert(false);//一般逻辑不会进入,用于检验平衡因子的异常
}

iv,右左双旋(新节点插入较高右子树的左侧---右左:先右单旋再左单旋)

         基本思路与左右双旋一致,这两种满足某种对称性,这里仅仅给出抽象图和代码实现:

右左双旋参考代码:

// 右左双旋
void RotateRL(Node* parent)
{Node* subR = parent->_right, * subRL = subR->_left;int bf = subRL->_bf;RotateR(subR);RotateL(parent);if (bf == 0){subRL->_bf = 0;subR->_bf = 0;parent->_bf = 0;}else if (bf == -1){parent->_bf = 0;subR->_bf = 1;subRL->_bf = 0;}else if (bf == 1){subRL->_bf = 0;parent->_bf = -1;subR->_bf = 0;}elseassert(false);
}

(四)AVL树的验证

        到这里,AVL树的主要工作原理你已经十分清楚了,接下来可以通过一下这个程序来测试一下我们的AVL树的代码逻辑有没有问题:

bool IsAVLTree()
{return _IsBalanceTree(_root);
}bool _IsBalanceTree(Node* root)
{// 空树也是AVL树if (nullptr == root)return true;// 计算pRoot节点的平衡因子:即pRoot左右子树的高度差int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);int diff = rightHeight - leftHeight;// 如果计算出的平衡因子与pRoot的平衡因子不相等,或者// pRoot平衡因子的绝对值超过1,则一定不是AVL树if (abs(diff) >= 2 || root->_bf != diff)return false;// pRoot的左和右如果都是AVL树,则该树一定是AVL树return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}size_t _Height(Node* root)
{if (root == nullptr)return 0;int Hl = _Height(root->_left), Hr = _Height(root->_right);return Hl > Hr ? Hl + 1 : Hr + 1;
}

 如果没有问题,那么在运行如下场景时,每一次插入后,检测都是满足AVL树的:

#include"AVLTree.h"
int main()
{ddsm::AVLTree<int,int> at;//int arr[] = {16, 3, 7, 11, 9, 26, 18, 14, 15};int arr[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };for (const auto& e : arr){at.insert({e,e});}at.inorder();cout<<at.IsAVLTree();return 0;
}

 运行结果:

4->1
2->1
6->1
1->1
3->1
5->1
15->1
7->1
16->1
14->1

 

(五)适用场景与性能分析

        AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即logN。

        但是如果要对AVL树做一些结构修改的操作,性能非常低下;(比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。)

        因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合使用AVL树来做底层来实现。

回顾:

        二叉搜索树——模拟实现

 


完~

未经作者同意禁止转载 

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

《程序猿入职必会(5) · CURD 页面细节规范 》

&#x1f4e2; 大家好&#xff0c;我是 【战神刘玉栋】&#xff0c;有10多年的研发经验&#xff0c;致力于前后端技术栈的知识沉淀和传播。 &#x1f497; &#x1f33b; CSDN入驻不久&#xff0c;希望大家多多支持&#xff0c;后续会继续提升文章质量&#xff0c;绝不滥竽充数…

为 Laravel 提供生产模式下的容器化环境:打造现代开发环境的终极指南

为 Laravel 提供生产模式下的容器化环境&#xff1a;打造现代开发环境的终极指南 在现代开发中&#xff0c;容器化已经成为一种趋势。使用 Docker 可以让我们轻松地管理和部署应用程序。本文将带你一步步构建一个高效的 Laravel 容器化环境&#xff0c;确保你的应用程序在开发…

一些Kafka面试题

Kafka是如何保证消息不丢失&#xff1f; 1.生产者发送消息到Broker丢失&#xff1a; 设置异步发送&#xff1a;发送失败则使用回调进行记录或者重发 消息重试&#xff1a;参数配置&#xff0c;可以设置重试次数 2.消息在broker中存储丢失 发送确认机制acks acks0&#xf…

谷粒商城实战笔记-MySQL踩坑记录

文章目录 1&#xff0c; Public Key Retrieval is not allowed问题描述解决办法 2&#xff0c;1044 -Access denied for user root% to database解决方案 1&#xff0c; Public Key Retrieval is not allowed 问题描述 打开DBeaver连接MySQL提示“Public Key Retrieval is no…

4款免费且安全:常用的PDF转Word在线转换工具推荐

现在办公越来越离不开电脑了&#xff0c;PDF文件和Word文档来回转换的需求也越来越大。作为一个天天跟文件打交道的上班族&#xff0c;我特别明白找个好用、靠谱的PDF转Word在线转换工具有多重要。今儿个&#xff0c;给大家说说五个免费的转换工具&#xff0c;都是我试过觉得挺…

多微信管理不再难:聚合聊天神器助你轻松应对!

在当今社交媒体高度发达的时代&#xff0c;很多人都在使用多个微信账号来管理个人与工作联系。面对如此众多的信息沟通&#xff0c;如何高效管理成了一个难题。 幸运的是&#xff0c;聚合聊天神器的出现&#xff0c;彻底改变了这一局面&#xff0c;让我们轻松应对多微信账号的…

接口测试框架中测试用例管理模块的优化与思考!

引言 在当今软件开发的快速迭代环境中&#xff0c;接口自动化测试不仅是确保软件质量的基石&#xff0c;更是推动持续集成&#xff08;CI&#xff09;和持续交付&#xff08;CD&#xff09;的核心环节。测试用例管理作为自动化测试中的重要模块&#xff0c;直接影响着测试的效…

【Python】面向对象的程序设计

一、面向对象的介绍 1.对象 对象是一种抽象概念&#xff0c;表示客观世界存在的实物&#xff0c;现实世界中能够看到的、触碰到的都可以成为对象&#xff0c;如&#xff1a;人、大象、小猫等。 对象通常分为两个部分&#xff0c;即静态部分和动态部分。静态部分为“属性”&a…

从教学到分享,2024精选录屏工具

如果你在公司里承担会议记录的职责&#xff0c;那录屏这项技能你一定要学会。像录屏大师这样的工具可以帮你在远程会议中进行录屏操作&#xff0c;方便你后期整理会议内容。 1.福昕录屏大师 链接直达&#xff1a;https://www.foxitsoftware.cn/REC/ 这款录屏工具提供了多种…

【Python】pandas:排序、重复值、缺省值处理、合并、分组

pandas是Python的扩展库&#xff08;第三方库&#xff09;&#xff0c;为Python编程语言提供 高性能、易于使用的数据结构和数据分析工具。 pandas官方文档&#xff1a;User Guide — pandas 2.2.2 documentation (pydata.org) 帮助&#xff1a;可使用help(...)查看函数说明文…

MyBatis入门如何使用操作数据库及常见错误(yml配置)

一&#xff0c;什么是MyBatis 是一款优秀的持久层框架&#xff0c;用于简化jdbc的开发 持久层&#xff1a;指的就是持久化操作的层&#xff0c;通常也就是数据访问层&#xff08;dao&#xff09;&#xff0c;也就是用来操作数据库。 也就是MyBatis是让你更加简单完成程序与数…

详细记录swfit微调interVL2-8B多模态大模型进行目标检测(附代码)

大模型相关目录 大模型&#xff0c;包括部署微调prompt/Agent应用开发、知识库增强、数据库增强、知识图谱增强、自然语言处理、多模态等大模型应用开发内容 从0起步&#xff0c;扬帆起航。 RAGOnMedicalKG&#xff1a;大模型结合知识图谱的RAG实现DSPy&#xff1a;变革式大模…

PRD: Peer Rank and Discussion Improve Large Language Model based Evaluations

文章目录 题目摘要相关工作方法实验与分析指标进一步分析结论 题目 PRD&#xff1a;同行排名和讨论改善基于大型语言模型的评估 论文地址&#xff1a;https://arxiv.org/abs/2307.02762 项目地址&#xff1a;https://openreview.net/forum?idYVD1QqWRaj 摘要 如今&#xff0c…

大模型深度神经网络(Deep Neural Network, DNN)

大模型深度神经网络&#xff08;Deep Neural Network, DNN&#xff09;是一种复杂的机器学习模型&#xff0c;其特点在于包含多个隐藏层&#xff0c;从而赋予模型强大的非线性表达能力和对复杂数据模式的学习能力。以下是对大模型DNN的详细介绍&#xff1a; 一、基本概念 深度…

第一阶段面试问题(前半部分)

1. 进程和线程的概念、区别以及什么时候用线程、什么时候用进程&#xff1f; &#xff08;1&#xff09;线程 线程是CPU任务调度的最小单元、是一个轻量级的进程 &#xff08;2&#xff09;进程 进程是操作系统资源分配的最小单元 进程是一个程序动态执行的过程&#xff0c;包…

MATLAB(6)水纹碰撞覆盖地形

前言 在MATLAB中模拟水纹&#xff08;如水波&#xff09;碰撞并覆盖地形的效果涉及到几个复杂的步骤&#xff0c;包括地形的生成、水波的模拟&#xff08;通常使用波动方程&#xff09;以及两者的交互。下面我将给出一个简化的示例&#xff0c;展示如何在MATLAB中创建一个基本的…

文献综述过程如何有助于综合各种来源的信息

VersaBot生成文献综述 文献综述过程在通过几个关键机制综合各种来源的信息方面发挥着至关重要的作用&#xff1b; 1. 批判性评估和比较&#xff1a; 你不能简单地单独总结每个来源&#xff1b;你积极地比较和对比他们的发现、方法和理论观点。这可以帮助您识别每个来源的共性…

安卓项目结构与日志工具

文章目录 安卓的项目结构app目录下的结构安卓的日志工具 安卓的项目结构 首先需要切换称Project模式。 .gradle和.idea &#xff1a;这两个目录下放置的都是Android Studio自动生成的一些文件&#xff0c;我们无须关心&#xff0c;也不用编辑。 app &#xff1a;项目中的代码、…

齿轮表面缺陷检测方案

齿轮是一种机械传动元件&#xff0c;通常由具有齿条的圆盘或圆柱体组成&#xff0c;用于传递动力和运动。齿轮通过齿与齿之间的啮合&#xff0c;将动力从一个轴传递到另一个轴&#xff0c;实现速度和扭矩的传递。齿轮通常用于机械设备、车辆传动系统和各种工业机械中。 齿轮通…

【网络世界】传输层协议

&#x1f308;前言&#x1f308; 欢迎收看本期【网络世界】&#xff0c;本期内容讲解TCP/IP协议栈中的传输层协议&#xff0c;即UDP协议和TCP协议。包含了他们的协议格式&#xff0c;特点等。介绍他们的应用场景&#xff0c;最后对比TCP与UDP协议。此外&#xff0c;还将介绍套接…