当前位置: 首页 > news >正文

【数据结构】 深入理解 LinkedList 与链表

文章目录

  • 一、链表:LinkedList 的底层基石
    • 1.1 链表的概念与结构特点
    • 1.2 链表的常见结构
  • 二、LinkedList 的模拟实现
    • 1. 接口设计(IList)
    • 2. 自定义异常(IndexException)
    • 3. MySingleList类
      • 3.1 基础结构定义
      • 3.2 增操作:头插、尾插、任意位置插
        • (1)头插法(addFirst)
        • (2)尾插法(addLast)
        • (3)任意位置插入(addIndex)
      • 3.3 查操作:包含判断与长度统计
        • (1)包含判断(contains)
        • (2)长度统计(size)
      • 3.4 删操作:删除单个节点与删除所有节点
        • (1)删除第一次出现的key(remove)
        • (2)删除所有值为key的节点(removeAllKey)
      • 3.5 其他工具方法
        • (1)遍历打印(display)
        • (2)清空链表(clear)
  • 三、LinkedList 详解
    • 3.1 LinkedList 的核心特性
    • 3.2 LinkedList 的构造方法
    • 3.3 LinkedList 的常用方法
    • 3.4 LinkedList 的遍历方式
  • 四、ArrayList 与 LinkedList 的区别

一、链表:LinkedList 的底层基石

1.1 链表的概念与结构特点

链表是一种物理存储结构非连续,但逻辑顺序通过节点间引用链接实现的线性数据结构。

  • 物理非连续:链表的节点在内存中不一定连续,每个节点包含数据域和引用域(指向其他节点)。
  • 逻辑连续:通过引用域,节点之间形成链式关系,保证了数据在逻辑上的连续。
  • 节点来源:现实中,链表的节点一般从堆内存中申请,两次申请的空间可能连续也可能不连续。

1.2 链表的常见结构

链表的结构多样,通过以下三个维度组合,可形成 8 种不同的链表结构:

  1. 单向 / 双向:单向链表节点只有一个引用域,指向后继节点;双向链表节点有两个引用域,分别指向前驱和后继节点。
  2. 带头 / 不带头:带头链表有一个头节点(不存储实际数据),用于简化操作;不带头链表直接从存储数据的节点开始。
  3. 循环 / 非循环:循环链表的尾节点引用指向头节点(或头节点相关节点),形成闭环;非循环链表的尾节点引用为 null。

在实际应用中,我们重点掌握两种核心结构:

  • 无头单向非循环链表:结构简单,一般不单独用于存储数据,常作为其他数据结构(如哈希桶、图的邻接表)的子结构,也是笔试面试中的高频考点。
  • 无头双向循环链表:Java 中 LinkedList 的底层实现就是这种结构,能兼顾插入、删除和查询操作的性能。

二、LinkedList 的模拟实现

LinkedList 基于无头双向循环链表实现,以下是其核心操作的模拟实现框架:

1. 接口设计(IList)

public interface IList {// 初始化链表(创建预设节点)void createList();// 头插法:在链表头部插入数据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. 自定义异常(IndexException)

针对「任意位置插入」时的非法下标(负数、超出链表长度),自定义运行时异常,增强错误提示的可读性。

public class IndexException extends RuntimeException {public IndexException(String message) {super(message);}
}

3. MySingleList类

3.1 基础结构定义

  • 内部静态类ListNode:封装节点的结构,避免对外暴露实现细节;
  • 成员变量head:作为链表的头节点引用。
public class MySingleList implements IList{// 内部静态类:链表节点static class ListNode{public int val;    // 数据域public ListNode next; // 指针域(下一个节点引用)// 节点构造器:初始化数据域public ListNode(int val) {this.val = val;}}ListNode head; // 头节点:链表的入口
}

3.2 增操作:头插、尾插、任意位置插

插入是单链表的核心操作,需重点处理「空链表」「头节点插入」等边界情况。

(1)头插法(addFirst)

逻辑:新节点的next指向原头节点,再将head更新为新节点(无论链表是否为空均适用)。
时间复杂度O(1)(无需遍历)。

@Override
public void addFirst(int data) {ListNode newNode = new ListNode(data); newNode.next = head;                  head = newNode;                   
}
(2)尾插法(addLast)

逻辑:先遍历到链表的最后一个节点(next = null的节点),再将其next指向新节点。
注意:若链表为空(head = null),直接将head指向新节点(避免cur.next空指针异常)。

@Override
public void addLast(int data) {ListNode newNode = new ListNode(data);// 边界:链表为空时,直接让head指向新节点if (head == null) {head = newNode;return;}// 遍历:找到最后一个节点(cur.next == null)ListNode cur = head;while (cur.next != null) {cur = cur.next;}cur.next = newNode; // 最后一个节点指向新节点
}
(3)任意位置插入(addIndex)

核心步骤

  1. 校验下标合法性(index < 0index > 链表长度均非法);
  2. 特殊位置复用已有方法:index=0复用addFirstindex=长度复用addLast
  3. 中间位置:先找到index-1处的前驱节点,再插入新节点(避免断链)。
@Override
public void addIndex(int index, int data) {int len = size();    if (index < 0 || index > len) {throw new IndexException("下标不合法!");}// 头插if (index == 0) {addFirst(data);return;}// 尾插if (index == len) {addLast(data);return;}// 中间位置:找到index-1的前驱节点ListNode cur = head;for (int i = 0; i < index - 1; i++) {cur = cur.next;}// 插入:新节点先连后,再让前驱连新节点(顺序不可反)ListNode newNode = new ListNode(data);newNode.next = cur.next;cur.next = newNode;
}

关键注意:插入顺序必须是「新节点先指向后一个节点」,再「前驱节点指向新节点」。若顺序颠倒,会导致后一个节点的引用丢失,造成链表断链。

3.3 查操作:包含判断与长度统计

(1)包含判断(contains)

逻辑:从head开始遍历所有节点,若找到值为key的节点则返回true,遍历结束未找到则返回false
时间复杂度O(n)(最坏需遍历整个链表)。

@Override
public boolean contains(int key) {ListNode cur = head;while (cur != null) { // 遍历终止条件:cur为null(已到链表末尾)if (cur.val == key) {return true;}cur = cur.next;}return false;
}
(2)长度统计(size)

逻辑:遍历链表,用计数器累计节点数量。
时间复杂度O(n)

@Override
public int size() {int cnt = 0;ListNode cur = head;while (cur != null) {cnt++;cur = cur.next;}return cnt;
}

3.4 删操作:删除单个节点与删除所有节点

删除操作的核心是「找到待删节点的前驱节点」,通过修改前驱节点的next引用跳过待删节点,实现节点移除。

(1)删除第一次出现的key(remove)

核心步骤

  1. 边界1:链表为空,直接返回;
  2. 边界2:头节点就是待删节点,直接将head指向head.next
  3. 常规情况:通过辅助方法findListNode找到待删节点的前驱,修改前驱的next引用。
// 辅助方法:找到值为key的节点的前驱(若未找到返回null)
ListNode findListNode(int key) {ListNode cur = head;// 遍历:判断cur的下一个节点是否为待删节点while (cur.next != null) {if (cur.next.val == key) {return cur;}cur = cur.next;}return null;
}@Override
public void remove(int key) {// 链表为空if (head == null) {return;}// 头节点为待删节点if (head.val == key) {head = head.next;return;}// 找到前驱节点ListNode cur = findListNode(key);if (cur == null) { // 未找到待删节点return;}// 前驱节点跳过待删节点ListNode del = cur.next;cur.next = del.next;
}
(2)删除所有值为key的节点(removeAllKey)

优化思路:用「双指针遍历」一次完成所有删除,避免多次遍历(效率更高):

  • prev:指向当前节点的前驱(初始为head);
  • cur:指向当前遍历的节点(初始为head.next)。

逻辑

  1. 先处理非头节点的待删节点(通过双指针跳过所有val=key的节点);
  2. 最后处理头节点(若头节点val=key,更新head)。
@Override
public void removeAllKey(int key) {// 边界:链表为空if (head == null) {return;}ListNode prev = head;ListNode cur = head.next;// 处理非头节点的待删节点while (cur != null) {if (cur.val == key) {// 待删节点:prev跳过cur,cur后移prev.next = cur.next;cur = cur.next;} else {// 非待删节点:双指针同时后移prev = cur;cur = cur.next;}}// 处理头节点(若头节点值为key)if (head.val == key) {head = head.next;}
}

3.5 其他工具方法

(1)遍历打印(display)

head开始遍历,打印每个节点的值,直观展示链表内容。

@Override
public void display() {ListNode cur = head;while (cur != null) {System.out.print(cur.val + " ");cur = cur.next;}System.out.println("  ");
}
(2)清空链表(clear)

逻辑:逐个断开节点的next引用(避免内存泄漏),最后将head置为null
注意:若直接将head = null,未断开的节点引用可能导致JVM无法回收内存。

@Override
public void clear() {// 边界:链表为空if (head == null) {return;}ListNode cur = head.next;ListNode curNext;// 逐个断开节点的next引用while (cur != null) {curNext = cur.next; // 先保存下一个节点cur.next = null;    // 断开当前节点的引用cur = curNext;      // 移到下一个节点}head = null; // 头节点置空,链表彻底为空
}

三、LinkedList 详解

3.1 LinkedList 的核心特性

  1. 底层基于无头双向循环链表实现,节点间通过引用关联,无需连续存储空间。
  2. 实现了 List 接口,支持 List 接口的所有操作。
  3. 未实现 RandomAccess 接口,不支持随机访问(即不能通过下标快速定位元素,需从头或尾遍历)。
  4. 任意位置插入和删除元素时,只需修改节点引用,时间复杂度为 O(1)(找到目标位置需 O(n),但修改操作本身是 O(1))。
  5. 无“容量”概念,无需像 ArrayList 那样进行扩容。

3.2 LinkedList 的构造方法

LinkedList 提供两种常用构造方法:

方法解释
LinkedList()无参构造,创建一个空的 LinkedList
LinkedList(Collection<? extends E> c)使用其他集合(如 ArrayList)中的元素构造 LinkedList

使用示例

public static void main(String[] args) {// 构造空的 LinkedListList<Integer> list1 = new LinkedList<>();// 先创建 ArrayList 并添加元素List<String> list2 = new ArrayList<>();list2.add("JavaSE");list2.add("JavaWeb");list2.add("JavaEE");// 使用 ArrayList 构造 LinkedListList<String> list3 = new LinkedList<>(list2);
}

3.3 LinkedList 的常用方法

LinkedList 提供了丰富的方法用于操作元素,以下是核心方法:

方法解释
boolean add(E e)尾插元素 e
void add(int index, E element)在 index 位置插入元素 element
boolean addAll(Collection<? extends E> c)尾插集合 c 中的所有元素
E remove(int index)删除 index 位置的元素,返回被删除的元素
boolean remove(Object o)删除遇到的第一个元素 o,返回是否删除成功
E get(int index)获取 index 位置的元素
E set(int index, E element)将 index 位置的元素设为 element,返回原元素
void clear()清空链表
boolean contains(Object o)判断元素 o 是否在链表中
int indexOf(Object o)返回第一个元素 o 的下标,若不存在返回 -1
int lastIndexOf(Object o)返回最后一个元素 o 的下标,若不存在返回 -1
List<E> subList(int fromIndex, int toIndex)截取 [fromIndex, toIndex) 区间的元素,返回新的 List

使用示例

public static void main(String[] args) {LinkedList<Integer> list = new LinkedList<>();// 尾插元素list.add(1);list.add(2);list.add(3);System.out.println("初始链表:" + list); // 输出:[1, 2, 3]System.out.println("链表长度:" + list.size()); // 输出:3// 在 index=0 位置插入元素 0list.add(0, 0);System.out.println("插入后链表:" + list); // 输出:[0, 1, 2, 3]// 删除操作list.remove(); // 默认删除第一个元素(调用 removeFirst())list.removeLast(); // 删除最后一个元素System.out.println("删除后链表:" + list); // 输出:[1, 2]// 判断元素是否存在if (!list.contains(0)) {list.add(0, 0);}System.out.println("添加后链表:" + list); // 输出:[0, 1, 2]// 查找元素下标System.out.println("第一个 0 的下标:" + list.indexOf(0)); // 输出:0// 修改元素list.set(0, 100);System.out.println("修改后链表:" + list); // 输出:[100, 1, 2]// 截取子链表List<Integer> subList = list.subList(0, 2);System.out.println("子链表:" + subList); // 输出:[100, 1]// 清空链表list.clear();System.out.println("清空后链表长度:" + list.size()); // 输出:0
}

3.4 LinkedList 的遍历方式

LinkedList 支持三种常见的遍历方式:

  1. foreach 遍历:简洁直观,适用于无需修改元素的场景。
  2. 迭代器遍历:支持正向和反向遍历,且在遍历过程中可安全修改元素(通过迭代器的 remove() 方法)。
  3. 普通 for 循环遍历:由于 LinkedList 不支持随机访问,通过下标获取元素会从头/尾遍历,效率较低,不推荐。

遍历示例

public static void main(String[] args) {LinkedList<Integer> list = new LinkedList<>();list.add(1);list.add(2);list.add(3);list.add(4);// 1. foreach 遍历for (int e : list) {System.out.print(e + " ");}System.out.println(); // 1 2 3 4// 2. 迭代器正向遍历ListIterator<Integer> it = list.listIterator();while (it.hasNext()) {System.out.print(it.next() + " ");}System.out.println(); // 1 2 3 4// 3. 迭代器反向遍历ListIterator<Integer> rit = list.listIterator(list.size());while (rit.hasPrevious()) {System.out.print(rit.previous() + " ");}System.out.println(); // 4 3 2 1
}

四、ArrayList 与 LinkedList 的区别

对比维度ArrayListLinkedList
存储空间物理上一定连续(基于数组)逻辑上连续,物理上不一定连续(基于链表)
随机访问支持,时间复杂度 O(1)(通过数组下标直接访问)不支持,时间复杂度 O(n)(需从头/尾遍历)
头插操作需搬移所有元素,时间复杂度 O(n)只需修改节点引用,时间复杂度 O(1)
插入操作空间不足时需扩容(默认扩容为原容量的 1.5 倍),可能浪费内存无容量概念,无需扩容,插入时直接创建节点
尾插操作若无需扩容,时间复杂度 O(1);若需扩容,时间复杂度 O(n)时间复杂度 O(1)(直接找到尾节点修改引用)
删除操作需搬移后续元素,时间复杂度 O(n)找到目标节点后,修改引用即可,时间复杂度 O(1)(查找过程 O(n))
内存占用可能存在内存浪费(扩容后未使用的空间)每个节点需额外存储前驱/后继引用,内存开销略大
应用场景元素存储稳定,频繁进行查询操作元素频繁进行任意位置插入/删除操作

文章转载自:

http://VDioRfEW.rmtbh.cn
http://Gi4qc6gM.rmtbh.cn
http://qGsqEn3d.rmtbh.cn
http://bumT0bZY.rmtbh.cn
http://tbZMr8ec.rmtbh.cn
http://flDbiZhP.rmtbh.cn
http://dPiTCC4c.rmtbh.cn
http://5OApgtoT.rmtbh.cn
http://OGUPqH6c.rmtbh.cn
http://a2wKXg8u.rmtbh.cn
http://GWKsQnly.rmtbh.cn
http://tb66A34s.rmtbh.cn
http://6sSY7ryw.rmtbh.cn
http://qnJEksdM.rmtbh.cn
http://ky9y7JyB.rmtbh.cn
http://XTvD42iO.rmtbh.cn
http://1mPtCYYh.rmtbh.cn
http://RdRpYDUs.rmtbh.cn
http://WhKZybD9.rmtbh.cn
http://4hNo0Isx.rmtbh.cn
http://N5DfQvj0.rmtbh.cn
http://jw8pGPoO.rmtbh.cn
http://4tjgEpgI.rmtbh.cn
http://RUrChoX7.rmtbh.cn
http://GUQF7tE3.rmtbh.cn
http://YX9oDI1Q.rmtbh.cn
http://ejbDS3wh.rmtbh.cn
http://8ZfRTNl8.rmtbh.cn
http://VCPbow8e.rmtbh.cn
http://pFQjGMVR.rmtbh.cn
http://www.dtcms.com/a/386028.html

相关文章:

  • Hadoop HDFS-高可用集群部署
  • 深入汇编底层与操作系统系统调用接口:彻底掰开揉碎c语言简单的一行代码-打印helloworld是如何从C语言点击运行到显示在屏幕上的
  • ARM3.(汇编函数和c语言相互调用及ARM裸机开发环境搭建)
  • LeetCode 380 - O(1) 时间插入、删除和获取随机元素
  • 9 基于机器学习进行遥感影像参数反演-以随机森林为例
  • DB Hitek宣布推出650V GaN HEMT工艺
  • 机器学习简单数据分析案例
  • [特殊字符] 欢迎使用 C++ Arrow 函数 - 革命性的新特性!
  • 外网访问分布式跟踪系统 zipkin
  • Base 发币在即:L2 代币能否撬动生态增长?
  • DRDR生态Token正式上线BitMart,开启全球化新篇章
  • Spring Boot 3 + EasyExcel 文件导入导出实现
  • 9.16总结
  • Android开机时间查看
  • 探针水平的表达矩阵转换为基因水平的表达矩阵是芯片数据分析中关键的一步
  • PHP基础-语法初步(第七天)
  • 奥威BI与ChatBI:自然语言交互赋能企业数据分析新体验
  • Vue: 组件基础
  • 亚马逊云科技 EC2 服务终端节点:安全高效访问云服务的利器
  • 2026届计算机毕业设计选题 大数据毕业设计选题推荐 题目新颖 数据分析 可视化大屏 通过率高
  • html实现文字横向对齐以及margin的解释
  • 如何轻松找到并畅玩Edge浏览器隐藏的冲浪小游戏
  • K8S中的神秘任务Job与CronJob
  • go grpc开发使用
  • [论文阅读] 人工智能 + 软件工程 | 告别冗余HTML与高算力消耗:EfficientUICoder如何破解UI2Code的token难题
  • Golang语言入门篇004_Go命令详解
  • K8S的Pod状态处理指南
  • Gin框架:构建高性能Go Web应用
  • Golang中的NaN(Not a Number)
  • golang 做webrtc开发核心