Java链表模拟实现+LinkedList介绍
文章目录
- 一、模拟实现单链表
- 成员属性
- 成员方法
- 0,构造方法
- 1,addFirst——头插
- 2,addLast——尾插
- 3,addIndex——在任意位置插入
- 3.1,checkIndex——判断index合法性
- 3.2,findPrevIndex——找到index-1位置的结点
- 4,contains——判定是否包含某个元素
- 5, remove——删除第一次出现关键字为key的结点
- 5.1, findPrevKey——找到key结点的前一个结点
- 6, removeAll——删除所有值为key的结点
- 7,size——获取单链表长度
- 8,clear——清空单链表
- 9,display——打印链表
- 二、Java提供的LinkedList
- 1,LinkedList 的说明
- 2,使用LinkedList
- 2.1,LinkedList 实例化方式
- 2.2,LinkedList 常用方法
- 链表和顺序表对比
- 总结
提示:是正在努力进步的小菜鸟一只,如有大佬发现文章欠佳之处欢迎评论区指点~ 废话不多说,直接发车~
一、模拟实现单链表
👉认识链表:
链表,是一种物理存储结构上 非连续 存储结构,数据元素的 逻辑顺序 是通过链表中的引用链接次序实现的 。
说人话:链表就是 “链接” 起来的。可以理解为火车,火车是由一节节车厢 “链接” 而成的;而链表,是由一个个结点/节点 “链接” 而成的
链表的结构可以细分为八种;我们主要掌握两种即可
1,无头单向非循环
结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多
2,无头双向循环
在Java的集合框架库中LinkedList底层实现就是无头双向循环链表
为什么要模拟实现:
自己模拟实现 简易版的 顺序表的增删查改等主要功能,大致理解顺序表的设计思想
再对比学习 Java 提供的集合类当中的 LinkedList,在学习 Java 的 LinkedList 常用方法的同时,也能学习源码的思想
我们模拟实现的是 无头单项非循环 链表
成员属性
Java 中的 LinkedList 是集合框架中的一个类,要模拟实现链表,也得自己实现一个类,首先要考虑这个类中的成员属性
已经说过,链表是由结点 “链接” 而成的,那么需要定义一个 静态内部类 Node:其中有两个域:
值域 value :来记录这个结点的值
指针域 next:来记录下一个结点的地址
还需要一个 成员变量:head 来记录当前链表的头结点
为了方便测试,全部设置为 public
public class MyLinkList {// 静态内部类,不依赖于链表对象,可以单独存在static class Node {public int value;public Node next;// Node类型的next值,存放下一个节点的地址public Node(int value) {this.value = value;}}public Node head;// 链表的头节点
⚠️模拟实现重在理解理想,为了简便,不使用泛型🙅,数组中存放 int 类型
成员方法
// 无头单向非循环链表实现public class MyLinkList {// 1,头插public void addFirst(int data){}// 2,尾插public void addLast(int data){}// 3,任意位置插入,第一个数据节点为0号下标public void addIndex(int index,int data){}// 4,查找是否包含关键字key是否在单链表当中public boolean contains(int key){}// 5,删除第一次出现关键字为key的节点public void remove(int key){}// 6,删除所有值为key的节点public void removeAllKey(int key){}// 7,得到单链表的长度public int size(){}// 8,清空链表public void clear() {}// 9,打印(官方没有这个方法,写出来方便我们自己测试)public void display() {}
}
0,构造方法
模拟的链表的这个类中构造方法只有内部类 Node 的构造方法,只用来对结点的 value 域赋值即可,指针域 next 不赋值,默认为 null ,在相应的方法内修改 Node 的指针域 next
// 静态内部类,不依赖于链表对象,可以单独存在static class Node {public int value;public Node next;// Node类型的next值,存放下一个节点的地址public Node(int value) {this.value = value;}}
1,addFirst——头插
❗️❗️和顺序表不同,链表是由一个个结点组成的,而顺序表可以理解为一个数组
顺序表插入之前必须考虑数组是否以及满了,而链表只需要关心各个结点的next即可
我们还有一个成员属性:head,是用来记录头结点的
链表的头插操作就是:
1,new 一个结点 node,类型是 Node
2,链接:把头结点的地址( head 的值)赋给 node 的指针域 next
3,head 记录新的头结点
public void addFirst(int value) {Node node = new Node(value);node.next = head;head = node;}
2,addLast——尾插
尾插步骤:
1,new 一个 node,类型是 Node
2,找到尾结点
3,链接:把 node 的值(也就是地址)赋给尾结点的指针域 next
如何找到尾结点呢?
需要 从头结点开始,遍历链表,找到一个结点的指针域 next 域为 null,它就是尾结点
head 是用来标记头结点的,所以 head 不能随意更改
我们需要再定义一个 Node 类型的 cur,让 cur 遍历链表
当 cur 找到尾结点后,需要让此时的尾结点和新结点 node 连接上
即:
cur.next = node;
如图:
cur 从头开始遍历链表
Node cur = head;
遍历链表的过程就是一个循环,如果cur.next == null 时跳出循环
如果不满足条件,说明cur此时不是尾结点,那么cur要往下走,如何实现呢?
cur 这个变量中存放的值 要更改为下一个结点的地址:cur = cur.next;
写到这里就要小心了,cur.next这一句代码是要访问 cur 的 next 域,如果 cur == null 时,这里就会发生空指针异常
那么有没有可能会这种情况呢?
cur 是从 head 开始往后遍历的,那么如果 head 一开始就是 null ,也就是链表为空时,cur 就会被 head 赋值成 null,就会发生空指针异常
所以当链表为空时,就不需要遍历链表找尾结点,直接把 node 的值赋给 head 即可
完整代码:
public void addLast(int value) {Node node = new Node(value);// 链表为空的情况if (head == null) {head = node;return;}Node cur = head;// 找到最后一个结点的位置while (cur.next != null) {cur = cur.next;}cur.next = node;}
3,addIndex——在任意位置插入
官方规定第一个数据的位置是0,和数组的位置(下标)规则一致
第一个参数就是 index,首先要判断 index 的合法性
3.1,checkIndex——判断index合法性
index<0 || index >链表长度是不合法的
public void checkIndex(int index) {if (index < 0 || index > size()) {// size()方法获取链表长度,遍历链表即可,比较简单,不多赘述throw new LinkListIndexOutOfException("这个位置不合法");// 异常处理}}
index 合法的情况下,如何在index位置插入删除呢?
index == 0就是头插,index = 链表长度就是尾插,主要是链表中间位置的插入和删除
要想在两个结点中间插入新结点,首先要找到这两个结点的地址
找到index -1结点的位置也就相当于找到了index结点的位置
3.2,findPrevIndex——找到index-1位置的结点
public Node findPrevIndex(int index) {Node cur = head;int count = 0;while (count != index - 1) {cur = cur.next;count++;}return cur;}
代码实现很简单,cur遍历链表index-1次即可
具体插入步骤:
1,node.next = prevIndex.next;
2,prevIndex.next = node;
这两行不能交换位置,如果先让 prevIndex.next = node;那么就会丢失 index 位置的那个结点,此时 node.next = prevIndex.next 就相当于 node.next = node;代码会发生错误
完整代码:
public void addIndex(int index, int value) {// 先检查index参数是否合法checkIndex(index);// 如果index == 0;头插即可if (index == 0) {addFirst(value);return;}// 如果index == size;尾插即可if (index == size()) {addLast(value);return;}// 中间插入删除Node node = new Node(value);// 先找到index-1位置的结点Node prevIndex = findPrevIndex(index);node.next = prevIndex.next;prevIndex.next = node;}
4,contains——判定是否包含某个元素
比较简单,遍历这个数组即可
public boolean contain(int key) {Node cur = head;while (cur != null) {if (cur.value == key) {return true;}cur = cur.next;}return false;}
因为这里我们存放的是 int 类型的变量,但 LinkedList 当中可以存放引用数据类型的
⚠️⚠️⚠️当表中是引用类型时,就不可以用“等号”比较,应该用 equals 方法
5, remove——删除第一次出现关键字为key的结点
1,如果链表为空就不能再删了
2,如果头结点就是要删除的 key 结点,直接 head 存放下一个结点的地址
3,如果链表其他结点是要删除的 key 结点,要先找到 key 结点的前一个结点
5.1, findPrevKey——找到key结点的前一个结点
// 找到key的前一个结点public Node findPrevKey(int key) {Node cur = head;while (cur.next != null) {// 注意循环判断条件 如果cur.next==null 说明cur此时是最后一个节点// 如果再访问下一个结点的value值,会空指针异常// 此时的cur的value值在上一次循环以及判断过了,所以cur走到最后一个结点时,是最后一趟循环if (cur.next.value == key) {return cur;}cur = cur.next;}return null;}
返回找到的这个结点即可
删除过程如图:
当 key 结点的前一个结点的 next 不再存放 key 结点地址时,key 结点此后不会再被使用,会被系统自动回收
完整代码如下:
public void remove(int key) {// 如果链表为空if (head == null) {return;}// 如果头结点就是keyif (head.value == key) {head = head.next;return;}// 首先要找到key的前一个结点Node prevKey = findPrevKey(key);if (prevKey == null) {return;}// 重新链接Node del = prevKey.next;prevKey.next = del.next;}
6, removeAll——删除所有值为key的结点
这里需要一个 prevCur 结点来记录 cur 的前一个结点
当 cur 是 key 结点时,key结点的前一个结点(也就是 prevCur)可以直接断开和 key 结点的链接,指向 key 结点的下一个
利用循环操作每个结点的 next 即可
public void removeAll(int key) {// 如果链表为空if (head == null) {return;}Node prevCur = head;Node cur = head.next;while (cur != null) {if (cur.value == key) {prevCur.next = cur.next;cur = cur.next;} else {prevCur = cur;cur = cur.next;}}// 如果头结点就是keyif (head.value == key) {head = head.next;}}
7,size——获取单链表长度
直接遍历链表即可
public int size() {Node cur = head;int count = 0;while (cur != null) {cur = cur.next;count++;}return count;}
8,clear——清空单链表
head 这个变量一直存放着链表的头结点位置,把head置空,就找不到此链表,那么链表中的所有结点都会被系统自动回收
public void clear() {head = null;}
9,display——打印链表
注意:LinkedList 中不存在该方法,为了方便看测试结果
public void disPlay() {Node cur = head;while (cur != null) {System.out.print(cur.value + " ");cur = cur.next;}System.out.println();}
二、Java提供的LinkedList
1,LinkedList 的说明
🙋🏼Java官方的集合框架中,LinkedList 是一个普通的类,继承了List接口
LinkedList 的底层是双向链表结构,由于链表没有将元素存储在连续的空间中,元素存储在单独的结点中,然后通过引用将结点连接起来了,因此在在任意位置插入或者删除元素时,不需要搬移元素,效率比较高。
⚠️特殊说明:
1 LinkedList 实现了List接口
2. LinkedList 的底层使用了双向链表
3. LinkedList 没有实现 RandomAccess 接口,因此 LinkedList 不支持随机访问
4. LinkedList 的任意位置插入和删除元素时效率比较高,时间复杂度为O(1)
5. LinkedList 比较适合任意位置插入的场景
2,使用LinkedList
2.1,LinkedList 实例化方式
1️⃣无参构造法
ArrayList<Integer> arrayList1 = new ArrayList<>();
3️⃣有参数——参数是其他 Collection
Collection是集合框架中的一个接口,实现了这个接口的类的对象就可以作为参数,说白了就是
可以把其他的顺序表,链表,栈,队列等等 作为参数传参,例如:
ArrayList<Integer> arrayList = new ArrayList<>();arrayList .add(1);arrayList .add(2);LinkedList<Integer> linkedList= new LinkedList<>(arrayList );
我先 new 了一个顺序表对象 arrayList ,在这个表中插入了 “1” “2” 两个数据,顺序表中的数据是顺序存储的
当 arrayList 作为参数传递时,linkedList 对象中就有了 arrayList 中的所有数据,linkedList 中的数据是链式存储❗️的
2.2,LinkedList 常用方法
在 main 方法中展示使用方式:
public class Test {public static void main(String[] args) {LinkedList<Integer> linkedList = new LinkedList<>();// 1,插入(默认是尾插)linkedList.add(1);linkedList.add(2);linkedList.add(3);linkedList.add(4);linkedList.add(5);System.out.println("1,尾插后:" + linkedList);// 2,任意位置插入linkedList.add(0, -1);System.out.println("2,在0位置插入后:" + linkedList);// 先new一个顺序表对象ArrayList<Integer> arrayList = new ArrayList<>();arrayList.add(1);arrayList.add(2);arrayList.add(3);// 3,插入其他Collection的所有元素(尾插)linkedList.addAll(arrayList);System.out.println("3,插入arrayLsit所有数据后:" + linkedList);// 4,删除任意位置的数据linkedList.remove(0);System.out.println("4,删除0位置数据后:" + linkedList);// 5,删除任意数据linkedList.remove(new Integer(1));System.out.println("5,删除1后:" + linkedList);// 6,获取任意位置的数据int ret = linkedList.get(0);System.out.println("6,获取0位置的数据:" + ret);// 7,更改任意位置的数据linkedList.set(0, 100);System.out.println("7,把0位置的数据更改为100后:" + linkedList);// 8,查看链表中是否存在该数据boolean bl = linkedList.contains(100);System.out.println("8,查看链表中是否存在100这个数据:" + bl);// 9,获取链表中任意数据第一次出现的位置int index = linkedList.indexOf(3);System.out.println("9,3这个数据第一次出现的位置:" + index);// 10,获取链表中任意数据最后一次出现的位置int lastIndex = linkedList.lastIndexOf(3);System.out.println("10,3这个数据最后一次出现的位置:" + lastIndex);// 11,截取List<Integer> list = linkedList.subList(1, 3);// 左闭右开System.out.println("11,截取linkedList中从1到3的数据:" + list);// 12,清空linkedList.clear();System.out.println("12,清空linkedList后:" + linkedList);}
}
运行结果:
链表和顺序表对比
链表和顺序表都有各自的优缺点,并且这两种结构的优缺点基本是互补的,所以不存在哪一种结构更优秀,只有在不同的场景,会有更加合适的结构。
总结
以上就是今天分享的关于数据结构中【链表】的内容,
一方面介绍了如何模拟实现简易的单链表,
一方面介绍了Java集合框架中的 LInkedList 类的基本使用,
并且分析了链表和顺序表的区别对比
如果本篇对你有帮助,请点赞收藏支持一下,小手一抖就是对作者莫大的鼓励啦🤪🤪🤪
上山总比下山辛苦
下篇文章见
相关文章:

Java链表模拟实现+LinkedList介绍
文章目录一、模拟实现单链表成员属性成员方法0,构造方法1,addFirst——头插2,addLast——尾插3,addIndex——在任意位置插入3.1,checkIndex——判断index合法性3.2,findPrevIndex——找到index-1位置的结点…...

MySQL——单表、多表查询
一、单表查询 素材: 表名:worker-- 表中字段均为中文,比如 部门号 工资 职工号 参加工作 等 CREATE TABLE worker ( 部门号 int(11) NOT NULL, 职工号 int(11) NOT NULL, 工作时间 date NOT NULL, 工资 float(8,2) NOT NULL, 政治面貌 varcha…...

关于表的操作 数据库(3)
目录 前期准备工作: 一、单表查询: 二、多表查询: 前期准备工作: 修改数据库的配置文件,,使其可以显示库名,其中//d代表当前使用的数据库名 注:vim /etc/my.cnf.d/mysql-server.c…...

C++:红黑树
红黑树的概念 红黑树是一棵二叉搜索树,但是红黑树通过增加一个存储位表示结点的颜色RED或BLACK。通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出2倍,因而是接近平衡的。 红黑树的性质 ⭐…...
每天一道算法题の中缀表达式
中缀表达式(、-、*、/) :中缀表达式是指操作符位于操作数之间的数学表达式。例如,在中缀表达式"2 3"中,操作符""位于操作数"2"和"3"之间。现给定一个中缀表达式,…...
Dar语法基础-泛型
泛型 如果查看基本数组类型 List 的 API 文档,您会发现该类型实际上是 List<E>。 <…> 表示法将 List 标记为泛型(或参数化)类型——具有正式类型参数的类型。 按照惯例,大多数类型变量的名称都是单字母的࿰…...
rt-thread------串口(一)配置
系列文章目录 rt-thread 之 fal移植 rt-thread 之 生成工程模板 文章目录系列文章目录前言一、串口的配置step1:通过串口名字找到串口句柄step2:配置串口参数step3:设置串口接收回调函数step4:打开串口设备前言 UART(…...

Android - 自动系统签名
一、系统签名 以下是两类应用开发场景: 普通应用开发:使用公司自定义 keystore 进行签名,如:微信、支付宝系统应用开发:使用 AOSP 系统签名或厂商自定义 keystore 进行签名,如:设置、录音 系…...

SSH 服务详解 (八)-- vscode 通过 SSH 远程连接 linux 服务器
vscode 通过 SSH 远程连接 linux 服务器 SSH服务详解(一)–Linux SSH 服务器与客户端的安装与启动 SSH服务详解(二)–使用私钥登录 SSH 服务器(免密登录) SSH 服务详解 (三)-- 使用 SSH 代理 SSH 服务详解 (四)-- 本地调用远程主机的命令 SSH 服务详解 (五)-- 远程文件拷贝…...
【PTA Advanced】1060 Are They Equal(C++)
目录 题目 Input Specification: Output Specification: Sample Input 1: Sample Output 1: Sample Input 2: Sample Output 2: 思路 C 知识点UP 代码 题目 If a machine can save only 3 significant digits, the float numbers 12300 and 12358.9 are considered …...

仿真与测试:通过Signal Builder模块生成输入信号
本文研究通过Signal Builder模块生成输入信号的方法。 文章目录1 生成输入信号2 仿真过程2.1 搭建被测模型2.2 搭建Signal Builder输入模块2.3 配置仿真log及仿真3 总结1 生成输入信号 在汽车的电控软件开发中,经常会在Simulink模型内部进行单元测试。单元测试的本…...

云计算培训靠谱吗?
怎么算靠谱的培训呢? 举个例子: 我想参加云计算培训找个工作,机构满足了我的要求,有工作了,但是不是做云计算相关的。 小强也参加了云计算培训,想学好云计算成为技术大牛,最后专业学得普普通…...

力扣SQL刷题10
目录标题618. 学生地理信息报告--完全不会的新题型1097. 游戏玩法分析 V - 重难点1127. 用户购买平台--难且不会618. 学生地理信息报告–完全不会的新题型 max()函数的功效:(‘jack’, null, null)中得出‘jack’,(nul…...

31 岁生日快乐,Linux!
Linux 迎来了 31 岁生日,所以和我一起庆祝 Linux 的 31 岁生日吧,喝上一杯好香槟和一个美味的蛋糕!虽然有些人不承认 8 月 25 日是 Linux 的生日,但我知道。1991 年 8 月 25 日,21 岁的芬兰学生 Linus Benedict Torval…...

分布式ID生成方案
文章目录前言一、分布式ID需要满足的条件二、分布式ID生成方式基于UUID数据库自增数据库集群数据库号段模式redis ID生成基于雪花算法(Snowflake)模式百度(uid-generator)美团(Leaf)滴滴(Tinyid…...

合宙Air103|fbd数据库| fskv - 替代fdb库|LuatOS-SOC接口|官方demo|学习(16):类redis的fbd数据库及fskv库
基础资料 基于Air103开发板:🚗 Air103 - LuatOS 文档 上手:开发上手 - LuatOS 文档 探讨重点 对官方社区库接口类redis的fbd数据库及fskv库的调用及示例进行复现及分析,了解两库的基本原理及操作方法。 软件及工具版本 Luat…...

【论文精读】Deep Residual Learning for Image Recognition
1 Degradation Problem💦 深度卷积神经网络在图像分类方面取得了一系列突破。深度网络自然地将低/中/高级特征和分类器以端到端的多层方式集成在一起,特征的“层次”可以通过堆叠层数(深度)来丰富。最近的研究揭示了网络深度是至关重要的,在具…...
Lesson2:基础语法、输出输入
一、基础语法 1、行结构 一个Python程序可分为许多逻辑行,一般来说:一个语句就是一行代码,不会跨越多行。 """比如下面的Python程序,一共有3个逻辑行,每一行都通过print()输出一个结果。""…...
android 9.0去掉前置摄像头闪光灯功能
1.1概述 在9.0的系统rom定制化开发中,在系统中camera2也是非常重要的一部分功能,在很多场合会用到camera2拍照视频,等等功能, 但是在使用过程中发现系统camera2在使用的时候,在前置摄像头进行拍照的时候,会出现闪光灯的情况,对于产品来说,者就是一个大问题,所以产品要求…...

静态分析工具Cppcheck在Windows上的使用
之前在https://blog.csdn.net/fengbingchun/article/details/8887843 介绍过Cppcheck,那时还是1.x版本,现在已到2.x版本,这里再总结下。 Cppcheck是一个用于C/C代码的静态分析工具,源码地址为https://github.com/danmar/cppcheck …...

Flask RESTful 示例
目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题: 下面创建一个简单的Flask RESTful API示例。首先,我们需要创建环境,安装必要的依赖,然后…...

TDengine 快速体验(Docker 镜像方式)
简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能,本节首先介绍如何通过 Docker 快速体验 TDengine,然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker,请使用 安装包的方式快…...

Appium+python自动化(十六)- ADB命令
简介 Android 调试桥(adb)是多种用途的工具,该工具可以帮助你你管理设备或模拟器 的状态。 adb ( Android Debug Bridge)是一个通用命令行工具,其允许您与模拟器实例或连接的 Android 设备进行通信。它可为各种设备操作提供便利,如安装和调试…...
DockerHub与私有镜像仓库在容器化中的应用与管理
哈喽,大家好,我是左手python! Docker Hub的应用与管理 Docker Hub的基本概念与使用方法 Docker Hub是Docker官方提供的一个公共镜像仓库,用户可以在其中找到各种操作系统、软件和应用的镜像。开发者可以通过Docker Hub轻松获取所…...
java 实现excel文件转pdf | 无水印 | 无限制
文章目录 目录 文章目录 前言 1.项目远程仓库配置 2.pom文件引入相关依赖 3.代码破解 二、Excel转PDF 1.代码实现 2.Aspose.License.xml 授权文件 总结 前言 java处理excel转pdf一直没找到什么好用的免费jar包工具,自己手写的难度,恐怕高级程序员花费一年的事件,也…...

iPhone密码忘记了办?iPhoneUnlocker,iPhone解锁工具Aiseesoft iPhone Unlocker 高级注册版分享
平时用 iPhone 的时候,难免会碰到解锁的麻烦事。比如密码忘了、人脸识别 / 指纹识别突然不灵,或者买了二手 iPhone 却被原来的 iCloud 账号锁住,这时候就需要靠谱的解锁工具来帮忙了。Aiseesoft iPhone Unlocker 就是专门解决这些问题的软件&…...

【CSS position 属性】static、relative、fixed、absolute 、sticky详细介绍,多层嵌套定位示例
文章目录 ★ position 的五种类型及基本用法 ★ 一、position 属性概述 二、position 的五种类型详解(初学者版) 1. static(默认值) 2. relative(相对定位) 3. absolute(绝对定位) 4. fixed(固定定位) 5. sticky(粘性定位) 三、定位元素的层级关系(z-i…...
macOS多出来了:Google云端硬盘、YouTube、表格、幻灯片、Gmail、Google文档等应用
文章目录 问题现象问题原因解决办法 问题现象 macOS启动台(Launchpad)多出来了:Google云端硬盘、YouTube、表格、幻灯片、Gmail、Google文档等应用。 问题原因 很明显,都是Google家的办公全家桶。这些应用并不是通过独立安装的…...
oracle与MySQL数据库之间数据同步的技术要点
Oracle与MySQL数据库之间的数据同步是一个涉及多个技术要点的复杂任务。由于Oracle和MySQL的架构差异,它们的数据同步要求既要保持数据的准确性和一致性,又要处理好性能问题。以下是一些主要的技术要点: 数据结构差异 数据类型差异ÿ…...

【OSG学习笔记】Day 16: 骨骼动画与蒙皮(osgAnimation)
骨骼动画基础 骨骼动画是 3D 计算机图形中常用的技术,它通过以下两个主要组件实现角色动画。 骨骼系统 (Skeleton):由层级结构的骨头组成,类似于人体骨骼蒙皮 (Mesh Skinning):将模型网格顶点绑定到骨骼上,使骨骼移动…...