从零开始学习数据结构—【链表】—【探索环形链的设计之美】
环形链表
文章目录
- 环形链表
- 1.结构图
- 2.具体实现
- 2.1.环形链表结构
- 2.2.头部添加数据
- 2.2.1.具体实现
- 2.2.2.测试添加数据
- 2.3.尾部添加数据
- 2.3.1.具体实现
- 2.3.2.添加测试数据
- 2.4.删除头部数据
- 2.4.1.具体实现
- 2.4.2.测试删除数据
- 2.5.删除尾部数据
- 2.5.1.具体实现
- 2.5.2.测试删除数据
- 2.6.根据内容删除节点
- 2.6.1.具体实现
- 2.7.遍历环形链表
- 2.7.1.迭代器遍历
- 2.7.2.使用递归进行遍历
- 2.7.3.测试
- 3.具体应用场景
- 3.1.优点
- 3.2.缺点
- 3.3.应用场景
1.结构图
双向环形链表带哨兵,这个时候的哨兵,可以当头,也可做尾
带哨兵双向循环链表:结构稍微复杂,实现简单。一般用来单独存储数据,实际中使用的链表数据结构都是带头双向链表。另外,这个结构虽然结构复杂,但是使用代码实现后会发现结构会带来很多优势。
双向环形链表是一种链式数据结构,其每个节点包含指向前一个节点和后一个节点的指针,形成了一个闭环。这意味着链表的尾部节点指向头部节点,而头部节点指向尾部节点,形成了一个环状的结构。
带哨兵的双向环形链表在头部和尾部都有一个特殊的哨兵节点,这个哨兵节点不存储任何数据,仅用于简化链表的操作。哨兵节点使得链表中始终存在一个不变的头部和尾部,即使链表为空也如此。具体而言:
- 头部哨兵节点: 位于链表的头部,它的前驱节点指向链表的尾部节点,而它的后继节点
指向链表的第一个真实节点。头部哨兵节点使得在头部执行操作时变得更加简单,不需要特殊处理链表为空的情况,也不需要区分头部和尾部的操作。 - 尾部哨兵节点: 位于链表的尾部,它的后继节点指向链表的头部节点,而它的前驱节点
指向链表的最后一个真实节点。尾部哨兵节点同样简化了尾部操作,使得在尾部进行插入、删除等操作更加方便。
带哨兵的双向环形链表在实现时通常会带来一些优势:
- 简化操作: 哨兵节点的存在使得对链表头部和尾部的操作变得更加统一和简化。不需要特别处理头部或尾部为空的情况,使得代码更加清晰和简洁。
- 增强鲁棒性: 哨兵节点可以避免出现空指针异常,因为链表中始终存在一个不变的头部和尾部。这增强了代码的鲁棒性和可靠性。
- 逻辑统一: 哨兵节点的存在使得链表的逻辑更加统一,不需要在特殊情况下单独处理头部或尾部节点,使得代码更加一致性和可读性。

2.具体实现
2.1.环形链表结构
public class DoubleLinkedListSentinel {private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());public DoubleLinkedListSentinel(){// 初始化时 环形连链表创建指向自身sentinel.next = sentinel;sentinel.prev = sentinel;}/*** 创建哨兵*/private Node sentinel = new Node(null,-1,null);private static class Node{Node prev; // 头指针Integer value; // 值Node next; // 尾指针public Node(Node prev, Integer value, Node next) {this.prev = prev;this.value = value;this.next = next;}}/*** 重写toString 用于Json输出* @return*/@Overridepublic String toString() {StringBuffer sb = new StringBuffer();Node p = sentinel.next;while (p != sentinel){sb.append(","+p.value);p = p.next;}// StringUtils.strip(sb.toString() 去除首位固定字符return "Node[ " + StringUtils.strip(sb.toString(), ",") +" ]";}
}
2.2.头部添加数据
# 思路找到哨兵,找到哨兵的下一个节点,创建新的对象(指定新节点的前后节点),将哨兵next指向新创建的节点,将哨兵的下一个节点指向新加的节点

2.2.1.具体实现
/*** 在头部添加值* @param value 待添加的元素*/public void addFirst(int value){// 找到哨兵Node head = sentinel;// 找到哨兵的下一个节点Node next = sentinel.next;// 创建新的对象Node addNode = new Node(head, value, next);// 将哨兵next指向新创建的节点head.next = addNode;//将哨兵的下一个节点的头节点指向新加的节点next.prev = addNode;}
2.2.2.测试添加数据
@Test
@DisplayName("测试双向环形链表")
public void test(){DoubleLinkedListSentinel node = new DoubleLinkedListSentinel();logger.error("add after node :{}",node);node.addFirst(1);node.addFirst(2);node.addFirst(3);logger.error("add after node :{}",node);
}

2.3.尾部添加数据
# 思路找到最后一个节点,找到头节点,创建新的节点(指明前后节点),将最后一个节点的next指向新创建的节点,将头节点的prev指向新创建的节点

2.3.1.具体实现
/*** 向链表的最后一个节点添元素* @param value 需要添加元素的值*/
public void addLast(int value){// 找到最后一个节点Node next = sentinel.prev;// 找到头节点Node head = sentinel;// 创建新的节点Node node = new Node(next, value, head);// 将最后一个节点的next指向新创建的节点next.next = node;// 将头节点的prev指向新创建的节点head.prev = node;
}
2.3.2.添加测试数据
@Test@DisplayName("测试-双向环形链表-尾部添加元素")public void tes2(){DoubleLinkedListSentinel node = new DoubleLinkedListSentinel();node.addLast(1);node.addLast(2);node.addLast(3);node.addLast(4);logger.error("linked list is: :{}",node);}

2.4.删除头部数据
# 思路 找到需要删除的节点,找到上一个节点,找到删除节点的下一个节点,将头节点的next指向删除节点的下一个节点,将删除节点的prev指向head

2.4.1.具体实现
/*** 删除第一个节点*/
public void removedFirst(){// 先找到需要删除的节点Node deleteNode = sentinel.next;// 如果删除的节点等于哨兵 那么不能删除if (deleteNode == sentinel){throw new IllegalArgumentException("delete node is null!");}// 找到上一个节点Node head = sentinel;// 找到删除节点的下一个节点Node next = deleteNode.next;// 将头节点的next指向删除节点的下一个节点head.next = next;// 将删除节点的prev指向headnext.prev = head;
}
2.4.2.测试删除数据
@Test
@DisplayName("测试-双向环形链表-删除第一个数据")
public void tes2(){DoubleLinkedListSentinel node = new DoubleLinkedListSentinel();node.addLast(1);node.addLast(2);node.addLast(3);node.addLast(4);logger.error("remove first node :{},size :{}",node,node.size());logger.error("------------------ remove ----------------");node.removedFirst();logger.error("remove first node :{},size :{}",node,node.size());node.removedFirst();logger.error("remove first node :{},size :{}",node,node.size());node.removedFirst();logger.error("remove first node :{},size :{}",node,node.size());node.removedFirst();logger.error("remove first node :{},size :{}",node,node.size());node.removedFirst();
}

2.5.删除尾部数据
# 思路找到最后一个节点,找到删除节点的上一个节点,找到删除节点的下一个节点,将删除节点的上一个节点 next指向头部,将哨兵执行最后一个节点

2.5.1.具体实现
/*** 删除最后一个节点*/public void removeLast(){// 找到最后一个节点Node deleteNode = sentinel.prev;// 如果删除的节点等于哨兵 那么不能删除if (deleteNode == sentinel){throw new IllegalArgumentException("delete node is null!");}// 找到删除节点的上一个节点Node head = deleteNode.prev;// 将删除节点的下一个节点Node next = sentinel;// 将删除的节点的上一个节点 next指向头部head.next = next;// 将哨兵执行最后一个节点next.prev = head;}
2.5.2.测试删除数据
@Test
@DisplayName("测试-双向环形链表-删除最后一个数据")
public void tes2(){DoubleLinkedListSentinel node = new DoubleLinkedListSentinel();node.addLast(1);node.addLast(2);node.addLast(3);node.addLast(4);logger.error("remove last node :{},size :{}",node,node.size());logger.error("------------------ remove ----------------");node.removeLast();logger.error("remove last node :{},size :{}",node,node.size());node.removeLast();logger.error("remove last node :{},size :{}",node,node.size());node.removeLast();logger.error("remove last node :{},size :{}",node,node.size());node.removeLast();logger.error("remove last node :{},size :{}",node,node.size());node.removeLast();
}

2.6.根据内容删除节点
# 思路找到需要删除的节点,找到删除节点的上一个节点,找到删除节点的下一个节点,将删除节点的上一个节点 next指向删除节点的下一个节点,将删除节点的下一个节点 prev指向删除节点的上一个节点

2.6.1.具体实现
@Test
@DisplayName("测试-双向环形链表-根据内容删除元素")
public void tes2(){DoubleLinkedListSentinel node = new DoubleLinkedListSentinel();node.addLast(1);node.addLast(2);node.addLast(3);node.addLast(4);int r1 = RandomUtils.nextInt(1, 5);int r2 = RandomUtils.nextInt(1, 10);logger.error("linked list :{}",node);int i = node.removeByIndex(r1);if (i == -1){logger.error("未找到需要删除的元素,{}",r1);}else {logger.error("删除成功,{}",r1);}int j = node.removeByIndex(r2);if (j == -1){logger.error("未找到需要删除的元素,{}",r2);}else {logger.error("删除成功,{}",r2);}logger.error("find linked list :{}",node);
}

2.7.遍历环形链表
2.7.1.迭代器遍历
// 实现 public class DoubleLinkedListSentinel implements Iterable<Integer> 接口 重写/*** 通过实现迭代器 进行循环遍历*/
@Override
public Iterator<Integer> iterator() {return new Iterator<Integer>() {Node p = sentinel.next;@Overridepublic boolean hasNext() {return p != sentinel;}@Overridepublic Integer next() {Integer value = p.value;p = p.next;return value;}};
}
2.7.2.使用递归进行遍历
/*** 递归遍历(递归遍历)*/
public void loop(Consumer<Integer> before,Consumer<Integer> after){recursion(sentinel.next,before,after);
}/*** 递归进行遍历* @param node 下一个节点* @param before 遍历前执行的方法* @param after 遍历后执行的方法* @deprecated 递归遍历,不建议使用,递归深度过大会导致栈溢出。建议使用迭代器,或者循环遍历,或者使用尾递归,或者使用栈* @see #loop(Consumer, Consumer)*/
public void recursion(Node node, Consumer<Integer> before, Consumer<Integer> after){// 表示链表没有节点了,那么就退出(注意 环形链表的 末尾 不是null 而是头节点)if (node == sentinel){return;}// 反转位置就是逆序了before.accept(node.value);recursion(node.next, before, after);after.accept(node.value);
}
2.7.3.测试
@Test@DisplayName("测试-双向环形链表-遍历")public void tes3(){DoubleLinkedListSentinel node = new DoubleLinkedListSentinel();node.addLast(1);node.addLast(2);node.addLast(3);node.addLast(4);logger.error("=========== 迭代器遍历链表 ===========");for (Integer i : node) {logger.error("迭代器遍历链表 :{}",i);}logger.error("=========== 递归遍历链表 ===========");node.loop(it->{logger.error("从头部开始遍历 :{}",it);},it ->{logger.error("从尾部开始遍历 :{}",it);});}

3.具体应用场景
3.1.优点
- 循环遍历简便: 由于双向环形链表形成了一个闭环,因此在需要循环遍历链表时,可以更加简便地实现,不需要额外的指针变量来记录链表的尾部或头部。
- 高效的插入和删除操作: 双向环形链表的节点结构允许在任意位置进行节点的插入和删除操作,并且这些操作通常比较高效,尤其是在头部和尾部进行操作时。
- 适用于循环结构数据: 对于需要处理循环结构的数据或需要实现环形队列等特定功能的场景,双向环形链表是一种很自然的数据结构选择。
3.2.缺点
- 强引用导致的内存泄漏: 如果双向环形链表中的节点持有对外部对象的强引用,并且这些外部对象的生命周期比链表更长,那么即使链表中的节点不再被使用,这些节点仍然被链表中的引用所持有,从而无法被垃圾回收器回收,导致内存泄漏。
- 未正确处理节点的引用关系: 在双向环形链表中,节点之间相互引用,如果在节点删除或者替换的过程中未正确地处理节点之间的引用关系,可能会导致链表中的节点无法被回收,从而引发内存泄漏。
- 长期持有迭代器: 如果在遍历双向环形链表的过程中长期持有迭代器对象,而没有正确地释放迭代器对象,可能会导致链表中的节点无法被回收,造成内存泄漏。
- 容易产生死循环: 由于环形链表的特性,编写循环遍历的代码时需要特别小心,如果没有正确地处理循环结束的条件,可能会产生死循环,导致程序崩溃或陷入无限循环。
- 实现复杂度较高: 相比于普通的单向链表,双向环形链表的实现复杂度较高,需要更多的代码来维护节点之间的引用关系,尤其是在节点的插入和删除操作时需要考虑更多的边界条件。
3.3.应用场景
- LRU Cache(最近最少使用缓存): 在LRU缓存中,双向环形链表可以用于维护最近使用的数据项的顺序。每次访问缓存中的数据时,可以将该数据项移到链表的头部,而最少使用的数据项则会被移动到链表的尾部,当缓存空间不足时,可以删除链表尾部的数据项。双向环形链表使得在链表头尾进行插入和删除操作更加高效。
- 循环队列: 在某些情况下,需要实现循环队列以存储和处理数据,比如在生产者-消费者模型中。双向环形链表可以用作循环队列的基础数据结构,使得在队列头尾进行数据插入和删除操作更加高效。
- 哈希表的冲突解决: 在哈希表中,如果多个键散列到相同的槽位上,就会发生冲突。双向环形链表可以用作哈希表中槽位的链表,用于解决冲突,实现链地址法(Separate Chaining)的哈希表。
相关文章:
从零开始学习数据结构—【链表】—【探索环形链的设计之美】
环形链表 文章目录 环形链表1.结构图2.具体实现2.1.环形链表结构2.2.头部添加数据2.2.1.具体实现2.2.2.测试添加数据 2.3.尾部添加数据2.3.1.具体实现2.3.2.添加测试数据 2.4.删除头部数据2.4.1.具体实现2.4.2.测试删除数据 2.5.删除尾部数据2.5.1.具体实现2.5.2.测试删除数据 …...
AJAX——HTTP协议
1 HTTP协议-请求报文 HTTP协议:规定了浏览器发送及服务器返回内容的格式 请求报文:浏览器按照HTTP协议要求的格式,发送给服务器的内容 1.1 请求报文的格式 请求报文的组成部分有: 请求行:请求方法,URL…...
java面试微服务篇
目录 目录 SpringCloud Spring Cloud 的5大组件 服务注册 Eureka Nacos Eureka和Nacos的对比 负载均衡 负载均衡流程 Ribbon负载均衡策略 自定义负载均衡策略 熔断、降级 服务雪崩 服务降级 服务熔断 服务监控 为什么需要监控 服务监控的组件 skywalking 业务…...
JS进阶——垃圾回收机制以及算法
版权声明 本文章来源于B站上的某马课程,由本人整理,仅供学习交流使用。如涉及侵权问题,请立即与本人联系,本人将积极配合删除相关内容。感谢理解和支持,本人致力于维护原创作品的权益,共同营造一个尊重知识…...
【快速解决】python项目打包成exe文件——vscode软件
目录 操作步骤 1、打开VSCode并打开你的Python项目。 2、在VSCode终端中安装pyinstaller: 3、运行以下命令使用pyinstaller将Python项目打包成exe文件: 其中your_script.py是你的Python脚本的文件名。 4、打包完成后,在你的项目目录中会…...
数据结构——lesson3单链表介绍及实现
目录 1.什么是链表? 2.链表的分类 (1)无头单向非循环链表: (2)带头双向循环链表: 3.单链表的实现 (1)单链表的定义 (2)动态创建节点 &#…...
中科大计网学习记录笔记(八):FTP | EMail
前言: 学习视频:中科大郑烇、杨坚全套《计算机网络(自顶向下方法 第7版,James F.Kurose,Keith W.Ross)》课程 该视频是B站非常著名的计网学习视频,但相信很多朋友和我一样在听完前面的部分发现信…...
QPaint绘制自定义坐标轴组件00
最终效果 1.创建一个ui页面,修改背景颜色 鼠标右键->改变样式表->添加颜色->background-color->选择合适的颜色->ok->Apply->ok 重新运行就可以看到widget的背景颜色已经改好 2.创建一个自定义的widget窗口小部件类,class MyChart…...
MATLAB|基于改进二进制粒子群算法的含需求响应机组组合问题研究(含文献和源码)
目录 主要内容 模型研究 1.改进二进制粒子群算法(BPSO) 2.模型分析 结果一览 下载链接 主要内容 该程序复现《A Modified Binary PSO to solve the Thermal Unit Commitment Problem》,主要做的是一个考虑需求响应的机组组合…...
JDBC核心技术
第1章 JDBC概述 第2章 获取数据库连接 第3章 使用PreparedStatement实现CRUD操作 第4章 操作BLOB类型字段 第5章 批量插入 第6章 数据库事务 第7章 DAO及相关实现类 第8章 数据库连接池 第9章 Apache-DBUtils实现CRUD操作图像 小部件...
【天幕系列 02】开源力量:揭示开源软件如何成为技术演进与社会发展的引擎
文章目录 导言01 开源软件如何推动技术创新1.1 开放的创新模式1.2 快速迭代和反馈循环1.3 共享知识和资源1.4 生态系统的建设和扩展1.5 开放标准和互操作性 02 开源软件的商业模式2.1 支持和服务模式2.2 基于订阅的模式2.3 专有附加组件模式2.4 开源软件作为平台模式2.5 双重许…...
“挖矿”系列:细说Python、conda 和 pip 之间的关系
继续挖矿,挖“金矿”! 1. Python、conda 和 pip(挖“金矿”工具) Python、conda 和 pip 是在现代数据科学和软件开发中常用的工具,它们各自有不同的作用,但相互之间存在密切的关系: Python&…...
【自然语言处理】实验3,文本情感分析
清华大学驭风计划课程链接 学堂在线 - 精品在线课程学习平台 (xuetangx.com) 代码和报告均为本人自己实现(实验满分),只展示主要任务实验结果,如果需要详细的实验报告或者代码可以私聊博主 有任何疑问或者问题,也欢…...
2.12日学习打卡----初学RocketMQ(三)
2.12日学习打卡 目录: 2.12日学习打卡一. RocketMQ高级特性(续)消息重试延迟消息消息查询 二.RocketMQ应用实战生产端发送同步消息发送异步消息单向发送消息顺序发送消息消费顺序消息全局顺序消息延迟消息事务消息消息查询 一. RocketMQ高级特…...
<网络安全>《35 网络攻防专业课<第一课 - 网络攻防准备>》
1 主要内容 认识黑客 认识端口 常见术语与命令 网络攻击流程 VMWare虚拟环境靶机搭建 2 认识黑客 2.1 白帽、灰帽和黑帽黑客 白帽黑客是指有能力破坏电脑安全但不具恶意目的黑客。 灰帽黑客是指对于伦理和法律态度不明的黑客。 黑帽黑客经常用于区别于一般(正面…...
【实战】一、Jest 前端自动化测试框架基础入门(一) —— 前端要学的测试课 从Jest入门到TDD BDD双实战(一)
文章目录 一、前端要学的测试课1.前端要学的测试2.前端工程化的一部分3.前端自动化测试的例子4.前端为什么需要自动化测试?5.课程涵盖内容6.前置技能7.学习收获 二、Jest 前端自动化测试框架基础入门1. 自动化测试背景及原理前端自动化测试产生的背景及原理 2.前端自…...
蓝桥杯Java组备赛(二)
题目1 import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner sc new Scanner(System.in);int n sc.nextInt();int max Integer.MIN_VALUE;int min Integer.MAX_VALUE;double sum 0;for(int i0;i<n;i) {int x sc.nextInt()…...
人力资源智能化管理项目(day10:首页开发以及上线部署)
学习源码可以看我的个人前端学习笔记 (github.com):qdxzw/humanResourceIntelligentManagementProject 首页-基本结构和数字滚动 安装插件 npm i vue-count-to <template><div class"dashboard"><div class"container"><!-- 左侧内…...
Conda管理Python不同版本教程
Conda管理Python不同版本教程 目录 0.前提 1.conda常用命令 2.conda设置国内源(以添加清华源为例,阿里云源同样) 3.conda管理python库 4.其它 不太推荐 pyenv管理Python不同版本教程(本人另一篇博客,姊妹篇&…...
free pascal:fpwebview 组件通过 JSBridge 调用本机TTS
从 https://github.com/PierceNg/fpwebview 下载 fpwebview-master.zip 简单易用。 先请看 \fpwebview-master\README.md cd \lazarus\projects\fpwebview-master\demo\js_bidir 学习 js_bidir.lpr ,编写 js_bind_speak.lpr 如下,通过 JSBridge 调用本…...
KART-RERANK大模型实战:Python爬虫数据智能排序与相关性分析
KART-RERANK大模型实战:Python爬虫数据智能排序与相关性分析 你是不是也遇到过这种情况?用Python爬虫吭哧吭哧抓了一大堆数据,结果发现里面什么都有:有用的、没用的、相关的、跑题的、高质量的、纯广告的……看着满屏的文本&…...
VibeVoice语音合成实战:流式播放+音频下载,打造个性化语音播报系统
VibeVoice语音合成实战:流式播放音频下载,打造个性化语音播报系统 1. 项目概述 VibeVoice-Realtime是微软开源的一款轻量级实时语音合成(TTS)模型,专为需要即时语音反馈的场景设计。这个只有0.5B参数的模型,却能在300毫秒内开始…...
当翻译成本趋近于零:AI原生时代,软件工程如何重塑?
当翻译成本趋近于零,软件工程的瓶颈就从“如何写对代码”变成了“如何定义对的事”。 一、两条路线之争:代码约束还是提示约束? 当前AI智能体演进中,出现了一条清晰的分野:以Claude Code为代表的“代码硬约束”路线&am…...
Copilot 命令行使用方式介绍(npm)
1. 什么是 Apache SeaTunnel? Apache SeaTunnel 是一个非常易于使用、高性能、支持实时流式和离线批处理的海量数据集成平台。它的目标是解决常见的数据集成问题,如数据源多样性、同步场景复杂性以及资源消耗高的问题。 核心特性 丰富的数据源支持&#…...
Learn Claude Code Agent 开发 | 2、插拔式工具系统:扩展功能不修改核心循环
Learn Claude Code Agent 开发 | 2、插拔式工具系统:扩展功能不修改核心循环 整体概述 多工具分发核心实现是基础智能体循环的直接扩展,核心思想就是: “加一个工具, 只加一个 handler” – 循环不用动, 新工具注册进 dispatch map 就行。 …...
Halcon角度计算双雄对比:orientation_region和smallest_rectangle2到底该用哪个?
Halcon角度计算双雄对比:orientation_region与smallest_rectangle2的实战抉择 在工业视觉检测中,区域角度计算是定位、对齐和测量的基础操作。Halcon作为机器视觉领域的标杆工具,提供了orientation_region和smallest_rectangle2两个核心算子来…...
nlp_structbert模型助力AIGC内容审核:生成文本与违规库相似度比对
nlp_structbert模型助力AIGC内容审核:生成文本与违规库相似度比对 1. 引言:当AIGC内容爆发,审核成了大难题 最近两年,AIGC技术发展得太快了。无论是写文章、做设计,还是生成营销文案,AI工具已经渗透到内容…...
Qwen3-ASR-1.7B在数学建模竞赛中的语音数据处理应用
Qwen3-ASR-1.7B在数学建模竞赛中的语音数据处理应用 数学建模竞赛,听起来是不是有点“高大上”?其实说白了,就是给你一个现实世界的问题,让你用数学和计算机的方法去解决。这几年,竞赛题目越来越贴近生活,…...
PP-DocLayoutV3入门必看:精准框定倾斜表格、弯曲公式、竖排文本的实操指南
PP-DocLayoutV3入门必看:精准框定倾斜表格、弯曲公式、竖排文本的实操指南 1. 认识新一代文档布局分析引擎 PP-DocLayoutV3是一个专门用于文档布局分析的智能工具,它能自动识别文档中的各种元素区域。想象一下,你有一张文档照片或扫描件&am…...
Nano-Banana在工业检测中的应用:产品缺陷自动识别与标注
Nano-Banana在工业检测中的应用:产品缺陷自动识别与标注 1. 引言 想象一下,在繁忙的生产线上,质检员需要每天检查成千上万的零件表面是否有划痕、凹陷或瑕疵。这种重复性工作不仅容易让人疲劳,还可能出现漏检误检的情况。传统的…...
