数据结构初阶--带头双向循环链表

目录

一.带头双向循环链表的定义

二.带头双向循环链表的功能实现

2.1.带头双向循环链表的定义

2.2.带头双向循环链表的结点创建

2.3.带头双向循环链表的初始化

2.4.带头双向循环链表的打印

2.5.带头双向循环链表的判空

2.6.带头双向循环链表的尾插

2.7.带头双向循环链表的头插

2.8.带头双向循环链表的尾删

2.9.带头双向循环链表的头删

2.10.带头双向循环链表的在pos位置之前插入

2.11.带头双向循环链表的删除pos位置的结点

2.12.带头双向循环链表的求链表长度

2.13.带头双向循环链表的销毁

2.14.完整程序

List.h

List.c

test.c

三. 顺序表和链表的比较

逻辑结构

存储结构

基本操作

创建

销毁

增加与删除

查找


一.带头双向循环链表的定义

循环单链表虽然能够实现从任一结点出发沿着链能找到其前驱结点,但时间耗费是O(n)。如果希望从表中快速确定某一个结点的前驱,另一个解决方法就是在单链表的每个结点里再增加一个指向其前驱的指针域prior。这样形成的链表中就有两条方向不同的链,称之为双(向)链表。

与单链表类似,双链表也可增加头结点使双链表的某些运算变得方便。同时双向链表也可以有循环表,称为双向循环链表。

由于在双向链表中既有前向链又有后向链,寻找任一结点的直接前驱结点与直接后继结点都变得非常方便了。

二.带头双向循环链表的功能实现

2.1.带头双向循环链表的定义

//定义
typedef int LTDataType;typedef struct ListNode
{struct ListNode* next;struct ListNode* pre;LTDataType data;
}LTNode;

 与单链表的定义不同,带头双向循环链表要定义两个指针:前驱指针pre和后继指针next。前驱指针pre用于指向当前结点的上一个结点,后继指针next用于指向当前结点的下一个结点。

2.2.带头双向循环链表的结点创建

LTNode* BuyListNode(LTDataType x)
{//动态开辟一个结点nodeLTNode* node = (LTNode*)malloc(sizeof(LTNode));//判空if (node == NULL){perror("malloc fail!");exit(-1);}//前驱与后继结点均置为空node->data = x;node->next = NULL;node->pre = NULL;return node;
}

结点的创建主要是通过调用malloc函数来实现,初始化时要将前驱指针和后继指针都置为NULL。

2.3.带头双向循环链表的初始化

版本一:

void ListInit(LTNode** phead)
{//这里需要传入二级指针,即传地址,才能实现对链表的修改//判空assert(phead);//创建头结点*phead = BuyListNode(-1);//将头结点的前驱指针和后继指针均指向自身(*phead)->next = *phead;(*phead)->pre = *phead;
}

版本二:

LTNode* ListInit()
{//创建头结点LTNode* phead = BuyListNode(-1);//将头结点的前驱指针和后继指针均指向自身phead->next = phead;phead->pre = phead;//返回头结点return phead;
}

链表的初始化采用了两种方式:传二级指针和设置返回值。

总结:

如果要改变头指针,就要传二级指针。不需要改变头指针的话,则传入一级指针。

在使用带头结点的单链表时:

  1. 初始化链表头指针需要传二级指针;
  2. 销毁链表需要传二级指针;
  3. 插入、删除、遍历、清空结点用一级指针即可。

不带头结点的单链表,除了初始化和销毁,插入、删除和清空结点也需要二级指针。

调试分析:

2.4.带头双向循环链表的打印

void ListPrint(LTNode* phead)
{//判空assert(phead);//cur指向链表的第一个结点LTNode* cur = phead->next;//cur依次向后遍历,直到cur重新回到头结点while (cur != phead){printf("%d ", cur->data);cur = cur->next;}printf("\n");
}

设置一个临时变量cur,指向当前链表的第一个结点(非头结点),然后依次向后遍历该链表,直到cur重新回到头结点phead的位置。

2.5.带头双向循环链表的判空

bool ListEmpty(LTNode* phead)
{//判空assert(phead);//如果phead->next等于phead,则链表为空,返回true//如果phead->next不等于phead,则链表不为空,返回falsereturn phead->next == phead;
}

如果phead->next等于phead,则链表为空,返回true;如果phead->next不等于phead,则链表不为空,返回false。

2.6.带头双向循环链表的尾插

void ListPushBack(LTNode* phead, LTDataType x)
{//判空assert(phead);//创建新结点LTNode* newnode = BuyListNode(x);//查找尾结点LTNode* tail = phead->pre;//尾插//原尾和新尾相互链接tail->next = newnode;newnode->pre = tail;//头结点和新尾相互链接newnode->next = phead;phead->pre = newnode;
}

相较于单链表的尾插,带头双向循环链表的尾插不需要从头结点开始依次向后遍历,因为头结点的前驱结点便指向尾结点tail。在找到尾结点tail之后,便可将新结点newnode插入到尾结点tail的后面。此时newnode变为新的尾结点。

调试分析:

运行结果:

2.7.带头双向循环链表的头插

void ListPushFront(LTNode* phead, LTDataType x)
{//判空assert(phead);//创建新结点LTNode* newnode = BuyListNode(x);//头插//phead newnode next:三者不分先后顺序//法一:LTNode* next = phead->next;phead->next = newnode;newnode->pre = phead;newnode->next = next;next->pre = newnode;//phead newnode phead->next:先处理后两个,再处理前两个//法二://phead->next->pre = newnode;//newnode->next = phead->next;//phead->next = newnode;//newnode->pre = phead;
}

在进行头插时,要注意结点之间插入的先后顺序,这里主要介绍两种方式。方式一:创建一个临时变量next,然后将头结点的下一个结点保存在next当中。首先调用BuyListNode(x)创建一个新结点newnode,然后将phead,newnode和next三个结点进行链接。三个结点不分先后顺序,直接进行链接即可。该方式最为简单,也最不容易出错;方式二:不创建临时变量next。首先调用BuyListNode(x)创建一个新结点newnode,然后将phead,newnode和phead->next三个结点进行链接。链接是关键:要先将后两个结点进行链接,然后再将前两个结点进行链接。三个结点一定要注意先后顺序,不可随意链接。

调试分析:

运行结果:

2.8.带头双向循环链表的尾删

void ListPopBack(LTNode* phead)
{//判空assert(phead);//判断链表是否为空assert(phead->next != phead);//assert(!ListEmpty(phead));//找尾结点LTNode* tail = phead->pre;//找尾结点的前一结点LTNode* tailPre = tail->pre;//释放尾结点free(tail);tailPre->next = phead;phead->pre = tailPre;
}

在进行尾删之前,首先要判断链表是否为空,可以通过phead->next != phead进行判断,也可以调用ListEmpty(phead)函数进行判断;然后找到链表的尾结点tail,以及链表尾结点的前一个结点tailPre;接着调用free函数释放尾结点tail,并将tailPre作为新的尾结点;最后再将新的尾结点与头结点phead进行相连即可。

调试分析:

运行结果:

2.9.带头双向循环链表的头删

void ListPopFront(LTNode* phead)
{//判空assert(phead);//判断链表是否为空assert(phead->next != phead);//assert(!ListEmpty(phead));//tail记录第一个结点之后的下一个结点LTNode* tail = phead->next->next;//释放第一个结点free(phead->next);//将头结点和tail相链接phead->next = tail;tail->pre = phead;
}

在进行头删之前,首先要判断链表是否为空,可以通过phead->next != phead进行判断,也可以调用ListEmpty(phead)函数进行判断;然后找到链表的第二个有效结点tail;接着调用free函数释放掉第一个有效结点,并将tail作为新的第一个有效结点;最后再将新的第一个结点tail与头结点phead进行相连即可。

调试分析:

运行结果:

2.10.带头双向循环链表的在pos位置之前插入

void ListInsert(LTNode* pos, LTDataType x)
{//判空assert(pos);//查找pos的前一个结点LTNode* pre = pos->pre;//创建新结点LTNode* newnode = BuyListNode(x);//pre newnode pospre->next = newnode;newnode->pre = pre;newnode->next = pos;pos->pre = newnode;
}

给定一个结点pos,如果是带头双向循环链表,那么pos之前的结点和pos之后的结点都是可知的。要在pos位置之前插入,首先要找到pos的前一结点pre,然后调用BuyListNode(x)创建一个新结点newnode,接着将pre,newnode和pos三个结点进行链接即可。此时pos位置的结点将由pos变为newnode。

调试分析:

运行结果:

2.11.带头双向循环链表的删除pos位置的结点

void ListErase(LTNode* pos)
{//判空assert(pos);//查找pos的前一个结点LTNode* pre = pos->pre;//查找pos的后一个结点LTNode* next = pos->next;//将前一个结点pre与后一个结点next相链接pre->next = next;next->pre = pre;//释放pos结点free(pos);
}

在删除pos位置的结点之前,首先要找到pos位置的前一个结点pre,然后找到pos位置的后一个结点next,接着将结点pre与next相链接,最后再调用free函数释放掉pos结点即可。

调试分析:

运行结果:

2.12.带头双向循环链表的求链表长度

int ListSize(LTNode* phead)
{//判空assert(phead);//cur指向当前链表的第一个结点LTNode* cur = phead->next;//用于记录遍历过的结点数int size = 0;//从第一个结点开始依次向后遍历,直到遍历到头结点while (cur != phead){++size;cur = cur->next;}return size;
}

调试分析:

运行结果:

2.13.带头双向循环链表的销毁

void ListDestory(LTNode* phead)
{//判空assert(phead);//cur指向当前第一个结点LTNode* cur = phead->next;while (cur != phead){//保存cur的下一个结点LTNode* next = cur->next;//删除curListErase(cur);//更新curcur = next;}//释放头结点free(phead);
}

调试分析:

运行结果:

2.14.完整程序

List.h

#pragma once#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>//带头双向循环链表//定义
typedef int LTDataType;typedef struct ListNode
{struct ListNode* next;struct ListNode* pre;LTDataType data;
}LTNode;//创建结点
LTNode* BuyListNode(LTDataType x);//初始化:版本一
//void ListInit(LTNode** phead);//初始化:版本二
LTNode* ListInit();//打印
void ListPrint(LTNode* phead);//判空
bool ListEmpty(LTNode* phead);//尾插
//不用二级指针的原因:尾插时不会改变phead,因为它带哨兵位,尾插时不会对哨兵位进行修改
void ListPushBack(LTNode* phead, LTDataType x);//头插
void ListPushFront(LTNode* phead, LTDataType x);//尾删
void ListPopBack(LTNode* phead);//头删
void ListPopFront(LTNode* phead);//在pos位置之前插入
void ListInsert(LTNode* pos, LTDataType x);//删除pos位置的结点
void ListErase(LTNode* pos);//链表长度
int ListSize(LTNode* phead);//销毁
void ListDestory(LTNode* phead);

List.c

#define _CRT_SECURE_NO_WARNINGS 1#include"List.h"//创建结点
LTNode* BuyListNode(LTDataType x)
{//动态开辟一个结点nodeLTNode* node = (LTNode*)malloc(sizeof(LTNode));//判空if (node == NULL){perror("malloc fail!");exit(-1);}//前驱与后继结点均置为空node->data = x;node->next = NULL;node->pre = NULL;return node;
}//初始化
/*
void ListInit(LTNode** phead)
{//这里需要传入二级指针,即传地址,才能实现对链表的修改//判空assert(phead);//创建头结点*phead = BuyListNode(-1);//将头结点的前驱指针和后继指针均指向自身(*phead)->next = *phead;(*phead)->pre = *phead;
}
*///初始化
LTNode* ListInit()
{//创建头结点LTNode* phead = BuyListNode(-1);//将头结点的前驱指针和后继指针均指向自身phead->next = phead;phead->pre = phead;//返回头结点return phead;
}//打印
void ListPrint(LTNode* phead)
{//判空assert(phead);//cur指向链表的第一个结点LTNode* cur = phead->next;//cur依次向后遍历,直到cur重新回到头结点while (cur != phead){printf("%d ", cur->data);cur = cur->next;}printf("\n");
}//判空
bool ListEmpty(LTNode* phead)
{//判空assert(phead);//如果phead->next等于phead,则链表为空,返回true//如果phead->next不等于phead,则链表不为空,返回falsereturn phead->next == phead;
}//尾插
void ListPushBack(LTNode* phead, LTDataType x)
{//判空assert(phead);/*//创建新结点LTNode* newnode = BuyListNode(x);//查找尾结点LTNode* tail = phead->pre;//尾插//原尾和新尾相互链接tail->next = newnode;newnode->pre = tail;//头结点和新尾相互链接newnode->next = phead;phead->pre = newnode;*///尾插ListInsert(phead, x);//是phead而不是phead->pre
}//头插
void ListPushFront(LTNode* phead, LTDataType x)
{//判空assert(phead);/*//创建新结点LTNode* newnode = BuyListNode(x);//头插//phead newnode next:三者不分先后顺序//法一:LTNode* next = phead->next;phead->next = newnode;newnode->pre = phead;newnode->next = next;next->pre = newnode;//phead newnode phead->next:先处理后两个,再处理前两个//法二://phead->next->pre = newnode;//newnode->next = phead->next;//phead->next = newnode;//newnode->pre = phead;*///头插ListInsert(phead->next, x);
}//尾删
void ListPopBack(LTNode* phead)
{//判空assert(phead);//判断链表是否为空assert(phead->next != phead);//assert(!ListEmpty(phead));/*//找尾结点LTNode* tail = phead->pre;//找尾结点的前一结点LTNode* tailPre = tail->pre;//释放尾结点free(tail);tailPre->next = phead;phead->pre = tailPre;*///尾删ListErase(phead->pre);
}//头删
void ListPopFront(LTNode* phead)
{//判空assert(phead);//判断链表是否为空assert(phead->next != phead);//assert(!ListEmpty(phead));/*//tail记录第一个结点之后的下一个结点LTNode* tail = phead->next->next;//释放第一个结点free(phead->next);//将头结点和tail相链接phead->next = tail;tail->pre = phead;*///头删ListErase(phead->next);
}//在pos位置之前插入x
void ListInsert(LTNode* pos, LTDataType x)
{//判空assert(pos);//查找pos的前一个结点LTNode* pre = pos->pre;//创建新结点LTNode* newnode = BuyListNode(x);//pre newnode pospre->next = newnode;newnode->pre = pre;newnode->next = pos;pos->pre = newnode;
}//删除pos位置的结点
void ListErase(LTNode* pos)
{//判空assert(pos);//查找pos的前一个结点LTNode* pre = pos->pre;//查找pos的后一个结点LTNode* next = pos->next;//将前一个结点pre与后一个结点next相链接pre->next = next;next->pre = pre;//释放pos结点free(pos);
}//链表长度
int ListSize(LTNode* phead)
{//判空assert(phead);//cur指向当前链表的第一个结点LTNode* cur = phead->next;//用于记录遍历过的结点数int size = 0;//从第一个结点开始依次向后遍历,直到遍历到头结点while (cur != phead){++size;cur = cur->next;}return size;
}//销毁
void ListDestory(LTNode* phead)
{//判空assert(phead);//cur指向当前第一个结点LTNode* cur = phead->next;while (cur != phead){//保存cur的下一个结点LTNode* next = cur->next;//法一:删除cur//ListErase(cur);//法二:删除curfree(cur);//更新curcur = next;}//释放头结点free(phead);
}

test.c

#define _CRT_SECURE_NO_WARNINGS 1#include"List.h"void test()
{LTNode* plist = NULL;//初始化plist = ListInit();//头插ListPushFront(plist, 1);ListPushFront(plist, 2);ListPushFront(plist, 3);ListPushFront(plist, 4);ListPushFront(plist, 5);ListPrint(plist);ListDestory(plist);ListPrint(plist);
}int main()
{test();return 0;
}

三. 顺序表和链表的比较

下面分别从逻辑结构,存储结构,基本操作的角度对顺序表和链表进行比较。

逻辑结构

都属于线性表,都是线性结构。

存储结构

基本操作

对于任何一个数据结构,基本操作基本都能归纳为创销,增删改查。其中改建立在查的基础上。

创建

销毁

增加与删除

查找

用链表还是顺序表

          顺序表          链表
弹性(可扩容)             ×           √
     增,删             ×           √
        改             √           ×

表长难以预估,经常需要增加/删除元素--链表

表长可预估,查询(搜索)操作较多--顺序表

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

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

相关文章

白皮书|数字孪生工厂:成就智能制造的未来

前言 目前我国已经进入“技术应用”的数字孪生双驱动时代&#xff0c;在市场需求和国家政策的推动下&#xff0c;各地因地制宜发展数字孪生技术&#xff0c;深入开展数字孪生应用建设。今天我们就重点来聊一聊数字孪生技术在工厂中的应用&#xff0c;希望通过本次案例的深度剖析…

深“扒”云原生高性能分布式文件系统JuiceFS

JuiceFS 是一款面向云原生设计的高性能分布式文件系统&#xff0c;在 Apache 2.0 开源协议下发布。提供完备的 POSIX 兼容性&#xff0c;可将几乎所有对象存储接入本地作为海量本地磁盘使用&#xff0c;亦可同时在跨平台、跨地区的不同主机上挂载读写。 JuiceFS 简介 JuiceFS…

推荐几款不错的AI绘画工具

随着近年来数据、算法等核心技术的不断进步&#xff0c;人工智能在内容创作各垂直领域的比例不断增加&#xff0c;包括人工智能写作、人工智能编辑和最近流行的人工智能绘画。 许多朋友也想跟上潮流&#xff0c;使用人工智能绘画生成软件创建人工智能图像&#xff0c;但我不知…

手机变局2023:一场瞄准产品和技术的“思维革命”

以折叠屏冲高端&#xff0c;已成为中国手机厂商们的共识。 在这个苹果未涉足的领域&#xff0c;国产手机厂商们加快脚步迭代推新&#xff0c;积极抢占机遇。但平心而论&#xff0c;虽然国产折叠屏机型众多&#xff0c;但市场上始终缺乏一款突破性的产品作为标杆&#xff0c;为…

数据结构【树】

第五章 树 一、树 1.定义&#xff1a;n个结点的集合&#xff0c;n0为空树&#xff1b;是递归的&#xff0c;是一种逻辑结构&#xff0c;同时也是一种分层结构&#xff1b; 特点 树的根结点没有前驱结点&#xff0c;除了根结点外的所有结点有且只有一个前驱结点&#xff1b;树…

Clion开发STM32之W5500系列(综合实验)

说明 此为w5500模块的综合实验测试模块,包含dhcp、dns、ntp以上三个模块的驱动参考之前的文章&#xff0c;本篇不做说明.使用的开发芯片 stm32f103vet6系列,外设接口使用的spi2 实验内容: 通过dhcp动态获取ip,通过dns解析NTP服务域名的ip通过NTP服务ip获取时间 w5500配置驱…

学生护眼台灯几瓦最舒适?2023全新五款护眼台灯推荐

每次经过学校&#xff0c;发现戴眼镜的小学生真的不少&#xff0c;通过近几年的儿童以及青少年的近视人数可看出&#xff0c;我国的近视人数中&#xff0c;儿童以及青少年占比53.5%&#xff0c;所以许多家长逐渐重视孩子用眼健康问题&#xff0c;会选择护眼台灯用于孩子写作业和…

第1章 获取数据库中的数据

CoreShop源程序是以数据库优先进行定义的&#xff0c;所以其本身不包含代码优先的定义&#xff0c;但本从更习惯于代码优先&#xff0c;所以为其定义了代码优先的定义。 1 CoreCms.Net.Model.Entities.SysRole using SqlSugar; using System.ComponentModel.DataAnnotations…

【JavaEE初阶】Tomcat安装与使用及初识Servlet

文章目录 1. Tomcat的安装与使用1.1 Tomcat安装1.2 Tomcat的启动1.3 Tomcat部署前端页面 2. Servlet2.1 Servlet是什么2.2 第一个Servlet程序2.3 常见错误 1. Tomcat的安装与使用 1.1 Tomcat安装 在浏览器中搜索Tomcat,打开官方网页.Tomcat官网 点击下载Tomcat8. 点击下载压…

PSP - 使用 MMseqs2 工具快速搜索蛋白质序列数据库 (GMGC)

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/131934642 MMseq2 是非常强大和高效的生物信息学软件&#xff0c;可以在极短的时间内对大规模的核苷酸和蛋白质序列进行搜索和聚类。主要特点有&a…

fpga_pwm呼吸灯(EP4CE6F17C8)

文章目录 一、呼吸灯二、代码实现三、引脚分配 一、呼吸灯 呼吸灯是指灯光在微电脑的控制之下完成由亮到暗的逐渐变化&#xff0c;使用开发板上的四个led灯实现1s间隔的呼吸灯。 二、代码实现 c module pwm_led( input clk ,input rst_n ,output reg [3:0] led ); …

c++网络编程:Boost.asio源码剖析

1、前言 Boost库是一个可移植、提供源代码的C库&#xff0c;作为标准库的后备&#xff0c;是C标准化进程的开发引擎之一。Boost库由C标准委员会库工作组成员发起&#xff0c;其中有些内容有望成为下一代C标准库内容。在C社区中影响甚大&#xff0c;是不折不扣的“准”标准库。…

未来行星探索希望:新型多脚机器人-团队版

机器人正在探索一个模拟的外星环境 即使一个机器人失败了&#xff0c;其余的团队成员也可以抵消它的损失。 背景 虽然探测器取得了令人难以置信的发现&#xff0c;但它们的轮子可能会拖慢它们的速度&#xff0c;而不稳定的地形可能会导致损坏。虽然没有东西可以取代“毅力号”…

Vue3输入框(Input)

APIs 参数说明类型默认值必传width输入框宽度string | number‘100%’falseaddonBefore设置前置标签string | slot‘’falseaddonAfter设置后置标签string | slot‘’falseallowClear可以点击清除图标删除内容booleanfalsefalsepassword是否启用密码框booleanfalsefalsedisabl…

两个小封装电机驱动芯片:MLX813XX、A4950

一&#xff0e;MLX813XX MELEXIS的微型电机驱动MLX813XX系列芯片集成MCU、预驱动以及功率模块等能够满足10W以下的电机驱动。 相对于普通分离器件的解决方案&#xff0c;MLX813XX系列电机驱动芯片是一款高集成度的驱动控制芯片&#xff0c;可以满足汽车系统高品质和低成本的要…

Spring Boot实践一

一、Spring Boot简介 Spring Boot是一个基于Spring框架的快速开发应用程序的工具。它提供了一种快速、方便的方式来创建基于Spring的应用程序&#xff0c;而无需繁琐的配置。Spring Boot通过自动配置和约定大于配置的方式&#xff0c;使得开发者可以更加专注于业务逻辑的实现&…

【CEEMDAN-WOA-LSTM】完备集合经验模态分解-鲸鱼优化-长短时记忆神经网络研究(Python代码实现)

目录 &#x1f4a5;1 概述 1.1 完备集合经验模态分解原理 1.2 鲸鱼优化 1.3 LSTM &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f308;4 Python代码实现 &#x1f4a5;1 概述 1.1 完备集合经验模态分解原理 早期的 EMD 方法具有较强的自适应性&#xff0c;能够有…

【node.js】03-http模块

目录 一、什么是http模块 二、创建基本的WEB服务器 三、req请求对象 四、res响应对象 五、根据不同的url响应不同的JSON内容 一、什么是http模块 http 模块是 Node.js 官方提供的、用来创建 web 服务器的模块。通过 http 模块提供的 http.createServer() 方法&#xff0c;…

深度学习推理和训练

优化和泛化 深度学习的根本问题是优化和泛化之间的对立。 • 优化&#xff08;optimization&#xff09;是指调节模型以在 训练数据 上得到最佳性能&#xff08;即机器学习中的学习&#xff09;。 • 泛化&#xff08;generalization&#xff09;是指训练好的模型在 前所未…

Java的0xFFFF在赋值、比较时引起的困惑

Java中的0xFFFF是整型&#xff0c;在赋值、比较的时候容易引起混淆&#xff0c;涉及到符号位、数值大小&#xff0c;赋值给什么类型的变量。我今天在编码的时候就遇到了一些困惑。用代码样例的形式记录下来&#xff0c;加深理解&#xff1a; package com.thb;public class Tes…