【数据结构】(6) LinkedList 链表

一、什么是链表

1、链表与顺序表对比

不同点LinkedListArrayList
物理存储上不连续连续
随机访问效率O(N)O(1)
插入、删除效率O(1)O(N)

3、链表的分类

        链表根据结构分类,可分为单向/双向无头结点/有头节点非循环/循环链表,这三组每组各取一个就构成一种结构链表。其中,单向、不带头、非循环链表学习的重点双向、不带头、非循环链表在实际开发中常用。

        以单向、不带头、非循环链表为例:

        双向、不带头、非循环链表:

        单向、带头、非循环链表(好处在于,不用特别处理头节点,比如增删时):

        单向、不带头、循环链表:

二、单向、不带头、非循环链表的实现

1、IList 接口

        定义 IList 接口,声明线性表需要实现的方法:

package listinterface;public interface IList {//头插法void addFirst(int data);//尾插法void addLast(int data);//任意位置插入,第一个数据节点为0号下标void addIndex(int index,int data);//查找是否包含关键字key是否在单链表当中boolean contains(int key);//删除第一次出现关键字为key的节点void remove(int key);//删除所有值为key的节点void removeAllKey(int key);//得到单链表的长度int size();void clear();void display();
}

2、结点内部类

        结点仅由链表内部使用,且每个结点的产生不依赖于某个链表,可定义为链表的私有静态内部类:

public class MySingleList implements IList {// 结点内部类private static class Node {int data;Node next;public Node(int data) {this.data = data;}}// 头结点private Node head;// 链表长度private int size;............
}

3、打印链表

    @Overridepublic void display() {Node cur = head;while (cur != null) {System.out.print(cur.data + " ");cur = cur.next;}System.out.println(" size: " + size);}

4、清空链表、获取链表长度

    @Overridepublic int size() {return size;}@Overridepublic void clear() {head = null;size = 0;}

        head 为空,没有引用指向 node1,自动回收 node1;node1 回收,没有引用指向 node2,自动回收 node2……故 clear 只需 head 设为 null 即可。

5、头插法

    @Overridepublic void addFirst(int data) {Node newNode = new Node(data);newNode.next = head;head = newNode;
//        if (head != null) {
//            newNode.next = head;
//        }
//        head = newNode;size++;}

6、尾插法

    @Overridepublic void addLast(int data) {Node newNode = new Node(data);if (head == null) {head = newNode;size++;return;}Node cur = head;while (cur.next != null) { // 找到尾节点cur = cur.next;}cur.next = newNode;size++;}

7、任意位置插入

受查异常,父类方法没抛出异常,子类方法不能抛出异常;父类抛出了,子类不做要求。非受查异常,不做要求。)为了不改动 IList,且让 addIndex 抛出异常,以便在 main 处对异常做处理,链表下标越界异常选择继承非受查异常 RuntimeExcption。

package myexception;public class ListIndexOutOfBoundsException extends RuntimeException {public ListIndexOutOfBoundsException(String message) {super(message);}
}
    private void checkIndexOutOfBounds(int index) throws ListIndexOutOfBoundsException {if (index < 0 || index > size) {throw new ListIndexOutOfBoundsException("插入位置不合法。" + "Index: " + index + ", Size: " + size);}}private Node findIndex(int index) {Node cur = head;int cnt = 0;while (cnt != index) {cur = cur.next;cnt++;}return cur;}@Overridepublic void addIndex(int index, int data) throws ListIndexOutOfBoundsException {// 先检查索引是否越界checkIndexOutOfBounds(index);if (index == 0) { // 在 0 处插入,没有前驱节点addFirst(data);return;}if (index == size) { // 在末尾插入addLast(data);return;}Node newNode = new Node(data);// 找到插入位置前的节点Node pre = findIndex(index - 1);// 插入新节点newNode.next = pre.next;pre.next = newNode;size++;}

8、删除第一次出现关键字为key的节点

    private Node findKey(int key) {Node cur = head;while (cur.next != null) {if (cur.next.data == key) {return cur;}cur = cur.next;}return null;}@Overridepublic void remove(int key) {if (head == null) { // 空链表return;}if (head.data == key) { // 头节点就是要删除的节点head = head.next;size--;return;}Node pre = findKey(key);if (pre != null) { // 找到了要删除的节点pre.next = pre.next.next;size--;} else { // 要删除的节点不存在System.out.println("要删除的节点不存在。");}}

9、删除所有关键字为 key 的节点

    @Overridepublic void removeAllKey(int key) {if (head == null) { // 空链表return;}Node pre = head;Node cur = head.next;while (cur != null) {if(cur.data == key) {pre.next = cur.next;// cur = cur.next;}else {pre = cur;// cur = cur.next;}cur = cur.next;}if (head.data == key) {head = head.next;size--;}}

三、链表面试题练习

1、反转链表

206. 反转链表 - 力扣(LeetCode)

思路:从头开始,边遍历边反转,最后尾结点为 head。

class Solution {public ListNode reverseList(ListNode head) {// 特殊处理 空链表、只有一个结点的链表if(head == null || head.next == null) { // 两者顺序不能反,因为 null 没有nextreturn head;}ListNode cur = head.next;// 头结点,其 next 为 null head.next = null;// 边反转边遍历,直到反转完尾结点while(cur != null) {ListNode curN = cur.next; cur.next = head;head = cur;cur = curN;}return head;}
}

2、链表的中间结点

876. 链表的中间结点 - 力扣(LeetCode)

思路

1、计算链表长度的一半,再走一半长。时间复杂度 O(N) + O(N/2)。

2、设置 slow 和 fast 从 head 开始走,slow 每次走 1 步,fast 每次走 2 步。那么 fast 走完全程(fast = null 或者 fast.next = null),必是 slow 的两倍长度,故 slow 就是中间节点的位置。时间复杂度 O(N/2) ,更优。

class Solution {public ListNode middleNode(ListNode head) { // 根据题目,链表非空// 从头节点开始ListNode slow = head;ListNode fast = head;// fast 走完整个链表while(fast != null && fast.next != null) {// slow 每次走 1 步,fast 每次走 2 步slow = slow.next;fast = fast.next.next;}// slow 就是中间结点return slow;}
}

3、返回倒数第 k 个结点

面试题 02.02. 返回倒数第 k 个节点 - 力扣(LeetCode)

思路:slow 从 head 开始,fast 比 slow 先多走 k-1 步,然后 slow、fast 每次只走一步,直到 fast 走到尾结点。

class Solution {public int kthToLast(ListNode head, int k) {// 根据题目,k 保证有效,因此不用判断 k<=0 和 k > len 的情况ListNode slow = head;ListNode fast = head;// 当 fast 先走 k-1 步int cnt = k-1;// k保证有效,fast 不会走过头while(cnt != 0) { fast = fast.next;cnt--;}// 让 fast 走到尾结点,slow 就是倒数第 k 个结点while(fast.next != null) {slow = slow.next;fast = fast.next;}return slow.val;}
}

 4、合并两个有序列表

21. 合并两个有序链表 - 力扣(LeetCode)

思路:分别同时遍历两个链表,更小的插入新链表,插入的链表插入后更新为 next,直到 l1 或者 l2 遍历完了。没遍历完的那个链表,把剩的接在新链表尾。为了不单独处理头指针,新链表带头节点。

class Solution {public ListNode mergeTwoLists(ListNode list1, ListNode list2) {ListNode head = new ListNode(); // 创建头节点ListNode cur = head;ListNode cur1 = list1;ListNode cur2 = list2;// 其中一个遍历完就退出while(cur1 != null && cur2 != null) {if(cur1.val < cur2.val) {cur.next = cur1;// cur = cur.next;cur1 = cur1.next;}else {cur.next = cur2;// cur = cur.next;cur2 = cur2.next;}cur = cur.next;}// 没遍历完的,接在后面if(cur1 != null) {cur.next = cur1;}if(cur2 != null) {cur.next = cur2;}return head.next; // 不带头节点}
}

5、链表分割

链表分割_牛客题霸_牛客网

思路:先创建两个链表,依次遍历原链表,小于和大于等于 x 的分开尾插入两个链表,再合并。

public class Partition {public ListNode partition(ListNode pHead, int x) {// 创建两个头节点ListNode head1 = new ListNode(-1); ListNode head2 = new ListNode(-1);// 遍历原链表,每个结点与 x 比大小,分类放在两个新链表中ListNode cur1 = head1;ListNode cur2 = head2; while(pHead != null) {if(pHead.val < x) {cur1.next = pHead;cur1 = cur1.next;}else {cur2.next = pHead;cur2 = cur2.next;}pHead = pHead.next;}// 现在 cur1、cur2 分别指向两个链表的尾结点。cur1.next = head2.next; // 链表1的尾接上链表2的头cur2.next = null; // 链表2的尾接上 null// 如果最后链表1为空,cur1就是head1,没问题// 如果最后链表2为空,cur2就是head2,没问题return head1.next;}
}

6、链表的回文结构

链表的回文结构_牛客题霸_牛客网

思路

1、求尾结点,头、尾同时前进并比较,到 head = tail(奇数长度) 或者 head.next = tail (偶数长度)为止。tail 往前走行不通,因为是单链表。

2、把原链表的后一半反转,再将反转链表与原链表对比,直到反转链表遍历完。时间复杂度O(N/2)+O(N/2)+O(N/2)=O(N)。找到一半处、将一半反转、对比一半。

public class PalindromeList {private ListNode findHalf(ListNode head) {ListNode slow = head;ListNode fast = head;while(fast != null && fast.next != null) {slow = slow.next;fast = fast.next.next;}return slow;}private ListNode reverse(ListNode head) {if(head == null || head.next == null) {return head;}ListNode cur = head.next;head.next = null;while(cur != null) {ListNode curN = cur.next;cur.next = head;head = cur;cur = curN;}return head;}public boolean chkPalindrome(ListNode A) {// 找到中间结点ListNode halfNode = findHalf(A);// 将后一半反转ListNode B = reverse(halfNode);// 将后半段与前半段对比,后半段遍历完退出while(B != null) {if(A.val != B.val) {return false;}A = A.next;B = B.next;}return true;}
}

7、相交链表

160. 相交链表 - 力扣(LeetCode)

思路:可能分叉的地方,前边或后边。但是对于链表不可能后边分叉,因为如果后边分叉了,当你遍历一条单链时,遍历到后边分叉的地方,就不知道继续走哪条路了,这样就不是单链表了。因此只有可能如上图的Y型,或者在一条直线上。

        如果同时各自出发,每次走一步,直到有一方到达尾结点,那么短的链表总是先到达,并且它们的最终距离是两链表的长度差。长度差来自分叉的部分,而不是相交的部分。那么如果让长的先走长度差步,再同时走,它们碰面的地方将是相交处。

public class Solution {private int size(ListNode head) {ListNode cur = head;int cnt = 0;while(cur != null) {cnt++;cur = cur.next;}return cnt;}public ListNode getIntersectionNode(ListNode headA, ListNode headB) {// 求两条链的长度int size1 = size(headA);int size2 = size(headB);// 让长的作为 headA,求两链表长度差(正),int len = size1 - size2;if(size1 < size2) {len = size2 - size1;ListNode tmp = headA;headA = headB;headB = tmp;}// 让长的先走 len 步while(len != 0) {headA = headA.next;len--;}// 再同时走,相等的地方就是相交结点while(headA != headB) {headA = headA.next;headB = headB.next;}return headA;}
}

8、环形链表

141. 环形链表 - 力扣(LeetCode)

思路:慢指针和快指针同时走,如果有环,那么快、慢指针总会在环里相遇(快指针多跑 k 圈,然后追上慢指针);如果没环,快指针先走完全程结束。我们设慢指针每次走1步,快指针每次走2步。

        为什么快指针不能是 3、4、……、n 步?如果是3步,存在以下情况,无论走多久都不会相遇:

        快指针如果走 4 步,存在以下情况,无论走多久都不会相遇:

        以此类推……

public class Solution {public boolean hasCycle(ListNode head) {// 设置快、慢指针ListNode slow = head;ListNode fast = head;// slow 每次走1步,fast 每次走2步// 直到 fast = null(偶数个) 或者 fast.next = null(奇数个) 返回 flase// 或者 slow = fast 返回 truewhile(fast != null && fast.next != null) {slow = slow.next;fast = fast.next.next;if(slow == fast) {return true;}}return false;}
}

9、环形链表Ⅱ

142. 环形链表 II - 力扣(LeetCode)

思路

        若一指针 head1 从相遇点开始,一指针 head2 从头指针开始,同时走,每次走一步。当 head1 走 X 步到达入口;同时,head2 从相遇点开始走了 (k-1) 圈回到相遇点,再走 N 步到入口。即两指针相遇处,就是入口结点。

public class Solution {private ListNode getMeetNode(ListNode head) {ListNode slow = head;ListNode fast = head;while(fast != null && fast.next != null) {slow = slow.next;fast = fast.next.next;if(slow == fast) {return slow;}}return null;}public ListNode detectCycle(ListNode head) {// 获得 slow 与 fast 的相遇结点ListNode meetNode = getMeetNode(head);// 链表无环,返回 nullif(meetNode == null) {return null;}// 分别从头指针,相遇结点开始走,两指针相遇处就是入口while(head != meetNode) {head = head.next;meetNode = meetNode.next;}return head;}
}

四、双向、不带头、非循环链表的实现

1、Node 内部类和属性

public class MyLinkedList  implements IList {private static class Node {int val;Node next; // 后继指针Node prev; // 前驱指针public Node(int val) {this.val = val;}}private Node head; // 头指针private Node tail; // 尾指针private int size; // 链表大小............
}

2、清空链表

        与单向不同直接 head = null,双向需要遍历结点并释放 node,原因如下:head 为空,还有 node2 指向 node1,所以手动置 node2 的 pre 为空,node1释放;还有 node3 指向 node2,手动置 node3 的 pre 未空……

    @Overridepublic void clear() {Node cur = head;while (cur!= null) {Node curN = cur.next;cur.prev = null;cur.next = null;cur = curN;}head = null;tail = null;size = 0;}

3、头插法

    @Overridepublic void addFirst(int data) {Node newNode = new Node(data);if (head == null) {head = newNode;tail = newNode;} else {newNode.next = head;head.prev = newNode;head = newNode;}size++;}

4、尾插法

    @Overridepublic void addLast(int data) {Node newNode = new Node(data);if (head == null) {head = newNode;tail = newNode;} else {tail.next = newNode;newNode.prev = tail;tail = newNode;}size++;}

5、任意位置插入

    @Overridepublic void addIndex(int index, int data) {// 先检查索引是否越界checkIndexOutOfBounds(index);if (index == 0) { // 在 0 处插入,没有前驱节点addFirst(data);return;}if (index == size) { // 在末尾插入addLast(data);return;}Node newNode = new Node(data);// 找到插入位置Node cur = findIndex(index);// 插入新节点newNode.next = cur;newNode.prev = cur.prev;cur.prev.next = newNode;cur.prev = newNode;size++;}

6、删除第一次出现的 key

    private Node findKey(int key) {Node cur = head;while (cur != null) {if (cur.val == key) {return cur;}cur = cur.next;}return null;}@Overridepublic void remove(int key) {Node deleteNode = findKey(key); // 找到待删除节点,如果不存在,返回 nullif (deleteNode == null) { // 包含空链表的情况System.out.println("要删除的节点不存在。");return;}if (deleteNode == head) { // 待删除节点是头节点,包含了链表只有一个结点的情况head = deleteNode.next;if (head == null) { // 链表只有一个节点tail = null;} else {head.prev = null;}} else if (deleteNode == tail) { // 待删除节点是尾节点tail = deleteNode.prev;tail.next = null;} else { // 待删除节点是中间节点deleteNode.prev.next = deleteNode.next;deleteNode.next.prev = deleteNode.prev;}size--;}

7、删除所有 key

    @Overridepublic void removeAllKey(int key) {Node cur = head;while (cur != null) {if (cur.val == key) {if (cur == head) { // 待删除节点是头节点head = cur.next;if (head == null) { // 链表中只有一个节点tail = null;}else {head.prev = null;}} else if (cur == tail) { // 待删除节点是尾节点tail = cur.prev;tail.next = null;} else { // 待删除节点是中间节点cur.prev.next = cur.next;cur.next.prev = cur.prev;}size--;
//                cur = cur.next; // 跳过已删除节点,继续遍历} /*else {cur = cur.next;}*/cur = cur.next;}}

五、LinkedList 的使用

        集合类中,LinkedList 的底层是双向链表

1、常用方法的使用

2、迭代器

        Iterator<E> 是集合类通用的迭代器,线性表专用的迭代器 ListIterator<E> 功能更强,可以反向迭代:

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

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

相关文章

.net8.0使用EF连接sqlite数据库及使用Gridify实现查询的简易实现

EF Core EF Core 是一个流行的对象关系映射&#xff08;ORM&#xff09;框架&#xff0c;它简化了与数据库的交互&#xff0c;提供了一个高效、灵活且易于使用的数据访问层。 Entity Framework (EF) Core 是轻量化、可扩展、开源和跨平台版的常用 Entity Framework 数据访问技…

【FPGA】 MIPS 12条整数指令 【3】

实现乘除 修改框架 EX&#xff1a;实现带符号乘除法和无符号乘除法 HiLo寄存器&#xff1a;用于存放乘法和除法的运算结果。Hi、Lo为32bit寄存器。电路描述与实现RegFile思想一致 仿真 代码 DataMem.v include "define.v"; module DataMem(input wire clk,input…

【回溯+剪枝】单词搜索,你能用递归解决吗?

文章目录 79. 单词搜索解题思路&#xff1a;回溯&#xff08;深搜&#xff09; 剪枝 79. 单词搜索 79. 单词搜索 ​ 给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 …

【蓝桥杯—单片机】第十届省赛真题代码题解题笔记 | 省赛 | 真题 | 代码题 | 刷题 | 笔记

第十届省赛真题代码部分 前言赛题代码思路笔记竞赛板配置内部振荡器频率设定键盘工作模式跳线扩展方式跳线连接频率测量功能的跳线帽 建立模板明确初始状态显示功能部分频率显示界面第一部分第二部分第三部分 电压显示界面第一部分第二部分第三部分 按键功能部分S4&#xff1a;…

使用DeepSeek的技巧笔记

来源&#xff1a;新年逼自己一把&#xff0c;学会使用DeepSeek R1_哔哩哔哩_bilibili 前言 对于DeepSeek而言&#xff0c;我们不再需要那么多的提示词技巧&#xff0c;但还是要有两个注意点&#xff1a;你需要理解大语言模型的工作原理与局限,这能帮助你更好的知道AI可完成任务…

Apache SeaTunnel 整体架构运行原理

概述 SeaTunnel 缘起 数据集成在现代企业的数据治理和决策支持中扮演着至关重要的角色。随着数据源的多样化和数据量的迅速增长及业务需求的快速变化&#xff0c;企业需要具备强大的数据集成能力来高效地处理数据。SeaTunnel通过其高度可扩展和灵活的架构&#xff0c;帮助企业…

1-kafka服务端之延时操作前传--时间轮

文章目录 背景时间轮层级时间轮时间轮降级kafka中的时间轮kafka如何进行时间轮运行 背景 Kafka中存在大量的延时操作&#xff0c;比如延时生产、延时拉取和延时删除等。Kafka并没有使用JDK自带的Timer或DelayQueue来实现延时的功能&#xff0c;而是基于时间轮的概念自定义实现…

【Ubuntu】ARM交叉编译开发环境解决“没有那个文件或目录”问题

【Ubuntu】ARM交叉编译开发环境解决“没有那个文件或目录”问题 零、起因 最近在使用Ubuntu虚拟机编译ARM程序&#xff0c;解压ARM的GCC后想要启动&#xff0c;报“没有那个文件或目录”&#xff0c;但是文件确实存在&#xff0c;环境配置也检查过了没问题&#xff0c;本文记…

[含文档+PPT+源码等]精品大数据项目-Django基于大数据实现的心血管疾病分析系统

大数据项目-Django基于大数据实现的心血管疾病分析系统背景可以从以下几个方面进行阐述&#xff1a; 一、项目背景与意义 1. 心血管疾病现状 心血管疾病是当前全球面临的主要健康挑战之一&#xff0c;其高发病率、高致残率和高死亡率严重威胁着人类的生命健康。根据权威机构…

科技赋能数字内容体验的核心技术探索

内容概要 在数字化时代&#xff0c;科技的迅猛发展为我们的生活和工作带来了深刻的变革。数字内容体验已经成为人们获取信息和娱乐的重要途径&#xff0c;而这背后的技术支持则扮演着至关重要的角色。尤其是在人工智能、虚拟现实和区块链等新兴技术的推动下&#xff0c;数字内…

【权重小技巧(3) 】权重替换—训练 A 模型去替换 B 模型中的对应权重

系列文章目录 【权重小技巧(1)】.pt文件无法打开或乱码&#xff1f;如何查看.pt文件的具体内容&#xff1f;【权重小技巧(2)】模型权重文件总结: .bin、.safetensors、.pt的保存、加载方法一览本文则总结权重的结构化读取和替换方法&#xff0c;以实现在框架 1 中训练后的部分…

VSCode中使用EmmyLua插件对Unity的tolua断点调试

一.VSCode中搜索安装EmmyLua插件 二.创建和编辑launch.json文件 初始的launch.json是这样的 手动编辑加上一段内容如下图所示&#xff1a; 三.启动调试模式&#xff0c;并选择附加的进程

k8sollama部署deepseek-R1模型,内网无坑

这是目录 linux下载ollama模型文件下载到本地,打包迁移到k8s等无网络环境使用下载打包ollama镜像非k8s环境使用k8s部署访问方式非ollama运行deepseek模型linux下载ollama 下载后可存放其他服务器 curl -L https://ollama.com/download/ollama-linux-amd64.tgz -o ollama-linu…

2025年Android NDK超全版本下载地址

Unity3D特效百例案例项目实战源码Android-Unity实战问题汇总游戏脚本-辅助自动化Android控件全解手册再战Android系列Scratch编程案例软考全系列Unity3D学习专栏蓝桥系列ChatGPT和AIGC &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分…

通信易懂唠唠SOME/IP——SOME/IP-SD服务发现阶段和应答行为

一 SOME/IP-SD服务发现阶划分 服务发现应该包含3个阶段 1.1 Initial Wait Phase初始等待阶段 初始等待阶段的作用 初始等待阶段是服务发现过程中的一个阶段。在这个阶段&#xff0c;服务发现模块等待服务实例的相关条件满足&#xff0c;以便继续后续的发现和注册过程。 对…

1. Kubernetes组成及常用命令

Pods(k8s最小操作单元)ReplicaSet & Label(k8s副本集和标签)Deployments(声明式配置)Services(服务)k8s常用命令Kubernetes(简称K8s)是一个开源的容器编排系统,用于自动化应用程序的部署、扩展和管理。自2014年发布以来,K8s迅速成为容器编排领域的行业标准,被…

Vue全流程--Vue2组件的理解第二部分

组件命名规则 好的命名规则可以省去很多不必要的麻烦&#xff0c;这个好习惯还是要养成的 一个单词组成&#xff1a; 第一种写法(首字母小写)&#xff1a;school 第二种写法(首字母大写)&#xff1a;School 多个单词组成&#xff1a; 第一种写法(kebab-case命名)&#xf…

【OS】AUTOSAR架构下的Interrupt详解(上篇)

目录 前言 正文 1.中断概念分析 1.1 中断处理API 1.2 中断级别 1.3 中断向量表 1.4 二类中断的嵌套 1.4.1概述 1.4.2激活 1.5一类中断 1.5.1一类中断的实现 1.5.2一类中断的嵌套 1.5.3在StartOS之前的1类ISR 1.5.4使用1类中断时的注意事项 1.6中断源的初始化 1.…

红包雨项目前端部分

创建项目 pnpm i -g vue/cli vue create red_pakage pnpm i sass sass-locader -D pnpm i --save normalize.css pnpm i --save-dev postcss-px-to-viewportpnpm i vantlatest-v2 -S pnpm i babel-plugin-import -Dhttps://vant.pro/vant/v2/#/zh-CN/<van-button click&…

深入理解k8s中的容器存储接口(CSI)

CSI出现的原因 K8s原生支持一些存储类型的PV&#xff0c;像iSCSI、NFS等。但这种方式让K8s代码与三方存储厂商代码紧密相连&#xff0c;带来不少麻烦。比如更改存储代码就得更新K8s组件&#xff0c;成本高&#xff1b;存储代码的bug还会影响K8s稳定性&#xff1b;K8s社区维护和…