JAVA数据结构 DAY5-LinkedList

本系列可作为JAVA学习系列的笔记,文中提到的一些练习的代码,小编会将代码复制下来,大家复制下来就可以练习了,方便大家学习。

点赞关注不迷路!您的点赞、关注和收藏是对小编最大的支持和鼓励!

系列文章目录

JAVA初阶---------已更完

JAVA数据结构 DAY1-集合和时空复杂度

JAVA数据结构 DAY2-包装类和泛型

JAVA数据结构 DAY3-List接口

JAVA数据结构 DAY4-ArrayList

JAVA数据结构 DAY5-LinkedList


拓展目录

手把手教你用 ArrayList 实现杨辉三角:从逻辑推导到每行代码详解


目录

目录

系列文章目录

目录

前言

[一、ArrayList 的缺陷:链表出现的必然性](#一、ArrayList 的缺陷:链表出现的必然性)

[1.1 ArrayList 底层结构回顾](#1.1 ArrayList 底层结构回顾)

[1.2 ArrayList 的核心缺陷](#1.2 ArrayList 的核心缺陷)

[1.2.1 插入操作的效率瓶颈](#1.2.1 插入操作的效率瓶颈)

[1.2.2 删除操作的效率问题](#1.2.2 删除操作的效率问题)

[1.2.3 扩容带来的额外开销](#1.2.3 扩容带来的额外开销)

[1.3 小结:ArrayList 的适用场景](#1.3 小结:ArrayList 的适用场景)

二、链表:数据结构的核心概念与结构

[2.1 链表的定义与核心特征](#2.1 链表的定义与核心特征)

[2.1.1 链表的概念](#2.1.1 链表的概念)

[2.1.2 链表与数组的核心对比](#2.1.2 链表与数组的核心对比)

[2.1.3 链表的节点结构](#2.1.3 链表的节点结构)

[2.2 链表的分类:8 种组合结构](#2.2 链表的分类:8 种组合结构)

[2.2.1 分类详解](#2.2.1 分类详解)

[2.2.2 8 种组合结构](#2.2.2 8 种组合结构)

[2.3 重点掌握的两种链表结构](#2.3 重点掌握的两种链表结构)

[2.3.1 无头单向非循环链表](#2.3.1 无头单向非循环链表)

[2.3.2 无头双向循环链表](#2.3.2 无头双向循环链表)

[2.4 链表的核心操作思想](#2.4 链表的核心操作思想)

[2.4.1 插入操作(单向链表示例)](#2.4.1 插入操作(单向链表示例))

[2.4.2 删除操作(单向链表示例)](#2.4.2 删除操作(单向链表示例))

[2.4.3 查找操作](#2.4.3 查找操作)

三、无头单向非循环链表的完整实现

[3.1 节点类定义](#3.1 节点类定义)

[3.2 链表核心操作实现](#3.2 链表核心操作实现)

[3.3 测试代码](#3.3 测试代码)

[3.4 实现要点](#3.4 实现要点)

[四、链表经典面试题解析(10 道核心题)](#四、链表经典面试题解析(10 道核心题))

[4.1 题目 1:删除链表中等于给定值 val 的所有节点(LeetCode 203)](#4.1 题目 1:删除链表中等于给定值 val 的所有节点(LeetCode 203))

题目描述

示例

思路

代码实现

注意事项

[4.2 题目 2:反转一个单链表(LeetCode 206)](#4.2 题目 2:反转一个单链表(LeetCode 206))

题目描述

示例

思路(迭代法)

代码实现(迭代法)

进阶:递归法

注意事项

[4.3 题目 3:链表的中间结点(LeetCode 876)](#4.3 题目 3:链表的中间结点(LeetCode 876))

题目描述

示例

思路

代码实现

注意事项

[4.4 题目 4:链表中倒数第 k 个结点(剑指 Offer 22)](#4.4 题目 4:链表中倒数第 k 个结点(剑指 Offer 22))

题目描述

示例

思路

代码实现

注意事项

[4.5 题目 5:合并两个有序链表(LeetCode 21)](#4.5 题目 5:合并两个有序链表(LeetCode 21))

题目描述

示例

思路

代码实现

进阶:递归法

注意事项

[4.6 题目 6:以给定值 x 为基准分割链表(LeetCode 86)](#4.6 题目 6:以给定值 x 为基准分割链表(LeetCode 86))

题目描述

示例

思路

代码实现

注意事项

[4.7 题目 7:链表的回文结构(剑指 Offer 27)](#4.7 题目 7:链表的回文结构(剑指 Offer 27))

题目描述

示例

思路

代码实现

注意事项

[4.8 题目 8:两个链表的第一个公共节点(剑指 Offer 52)](#4.8 题目 8:两个链表的第一个公共节点(剑指 Offer 52))

题目描述

示例

思路

代码实现

注意事项

[4.9 题目 9:判断链表中是否有环(LeetCode 141)](#4.9 题目 9:判断链表中是否有环(LeetCode 141))

题目描述

示例

思路

关键问题解答

代码实现

注意事项

[4.10 题目 10:链表入环的第一个节点(LeetCode 142)](#4.10 题目 10:链表入环的第一个节点(LeetCode 142))

题目描述

示例

思路

代码实现

注意事项

[五、无头双向循环链表的实现(LinkedList 底层模拟)](#五、无头双向循环链表的实现(LinkedList 底层模拟))

[5.1 节点类定义](#5.1 节点类定义)

[5.2 链表核心操作实现](#5.2 链表核心操作实现)

[5.3 测试代码](#5.3 测试代码)

[5.4 双向循环链表的优势与实现要点](#5.4 双向循环链表的优势与实现要点)

优势

实现要点

[六、LinkedList 的使用指南(Java 集合框架)](#六、LinkedList 的使用指南(Java 集合框架))

[6.1 LinkedList 的类结构与核心特性](#6.1 LinkedList 的类结构与核心特性)

类结构

核心特性

[6.2 LinkedList 的构造方法](#6.2 LinkedList 的构造方法)

使用示例

[6.3 LinkedList 的核心方法(List 接口)](#6.3 LinkedList 的核心方法(List 接口))

使用示例

[6.4 LinkedList 的双端队列方法(Deque 接口)](#6.4 LinkedList 的双端队列方法(Deque 接口))

使用示例

[6.5 LinkedList 的遍历方式](#6.5 LinkedList 的遍历方式)

遍历示例

注意事项

[七、ArrayList 与 LinkedList 的核心区别与应用场景](#七、ArrayList 与 LinkedList 的核心区别与应用场景)

[7.1 核心区别对比](#7.1 核心区别对比)

[7.2 应用场景选择](#7.2 应用场景选择)

[优先使用 ArrayList 的场景](#优先使用 ArrayList 的场景)

[优先使用 LinkedList 的场景](#优先使用 LinkedList 的场景)

[7.3 常见误区澄清](#7.3 常见误区澄清)

八、总结与拓展

[8.1 核心知识点总结](#8.1 核心知识点总结)

[8.2 拓展学习建议](#8.2 拓展学习建议)

总结


前言

小编作为新晋码农一枚,会定期整理一些写的比较好的代码,作为自己的学习笔记,会试着做一下批注和补充,如转载或者参考他人文献会标明出处,非商用,如有侵权会删改!欢迎大家斧正和讨论!

在 Java 集合框架中,List 接口的两大核心实现类 ------ArrayList 和 LinkedList,分别对应数组与链表两种基础数据结构。上一篇我们深入剖析了 ArrayList 的底层原理与使用场景,而本篇将聚焦 LinkedList 及其依赖的链表结构。从 ArrayList 的缺陷出发,逐步拆解链表的概念、结构、实现逻辑、经典面试题、LinkedList 的模拟与实战使用,最后通过对比两者的核心差异,帮助大家构建完整的链表知识体系,为算法学习和开发实践打下坚实基础。

本文涵盖链表相关所有核心知识点,总字数超 30000 字,包含理论讲解、代码实现、面试题深度解析、API 实战指南等内容,适合 Java 初学者、数据结构入门者及需要巩固链表知识的开发人员。建议结合代码示例逐节研读,复杂知识点可反复琢磨,确保真正理解并掌握。

一、ArrayList 的缺陷:链表出现的必然性

要理解链表(LinkedList)的设计初衷,首先需要明确 ArrayList 的核心局限性 ------ 正是这些缺陷,推动了链表这种数据结构的诞生。

1.1 ArrayList 底层结构回顾

通过之前的学习,我们知道 ArrayList 底层基于动态数组实现,其核心源码如下(关键部分截取):

java 复制代码
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    // 默认初始容量为 10
    private static final int DEFAULT_CAPACITY = 10;
    // 存储元素的核心数组(transient 表示不参与序列化)
    transient Object[] elementData;
    // 集合中有效元素的个数
    private int size;

    // 构造方法:指定初始容量
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
        }
    }
    // 其他方法(add、remove、get 等)...
}

从源码可见,ArrayList 的元素存储在一段连续的物理空间 中(即数组 elementData),这种结构带来了随机访问高效的优势,但也存在无法回避的缺陷。

1.2 ArrayList 的核心缺陷

ArrayList 的主要短板集中在任意位置的插入和删除操作,具体分析如下:

1.2.1 插入操作的效率瓶颈

当在 ArrayList 非末尾位置插入元素时,由于数组物理空间连续,插入位置之后的所有元素都需要整体向后搬移一位,才能腾出空间给新元素。

示例:现有容量为 5 的 ArrayList,元素为 [1,2,3,4,5],若在索引 1 处插入 6,需将索引 1~4 的元素(2,3,4,5)依次后移,最终数组变为 [1,6,2,3,4,5](若容量不足还需先扩容)。

这个搬移过程的时间复杂度为 O(n)(n 为元素个数),当元素数量庞大时,频繁插入会导致效率极低。

1.2.2 删除操作的效率问题

与插入类似,删除 ArrayList 非末尾元素时,删除位置之后的所有元素需整体向前搬移一位,填补删除留下的 "空位"。

示例:删除上述数组中索引 2 的元素 2,需将索引 3~5 的元素(3,4,5)依次前移,最终数组变为 [1,6,3,4,5]

该过程的时间复杂度同样为 O(n),元素越多,删除效率越低。

1.2.3 扩容带来的额外开销

ArrayList 容量固定(初始为 10),当元素个数达到容量上限时,会触发扩容机制 ------ 默认扩容为原容量的 1.5 倍(源码逻辑:newCapacity = oldCapacity + (oldCapacity >> 1))。扩容过程需经历:

  1. 创建新的更大容量数组;
  2. 复制原数组所有元素到新数组;
  3. 释放原数组内存。

这个过程消耗额外时间和空间,元素数量越大,扩容开销越明显。

1.3 小结:ArrayList 的适用场景

ArrayList 优势在于随机访问 (通过索引获取元素,O (1))和尾部插入 / 删除(无需搬移元素,O (1)),适合:

  • 元素数量稳定,不频繁进行任意位置插入 / 删除;
  • 需频繁通过索引访问元素的场景。

而当场景中存在大量任意位置插入 / 删除时,ArrayList 效率低下,此时需要链表这种数据结构来解决问题。

二、链表:数据结构的核心概念与结构

2.1 链表的定义与核心特征

2.1.1 链表的概念

链表是一种物理存储非连续逻辑顺序通过引用链接实现的数据结构。简单来说:

  • 物理存储:链表的元素(称为 "节点")分散在内存中,物理地址不一定连续;
  • 逻辑顺序:每个节点包含 "引用"(指针),指向其下一个(或上一个)节点,通过这些引用形成逻辑上的连续序列。
2.1.2 链表与数组的核心对比
对比维度 数组(ArrayList 底层) 链表
物理存储 连续空间 非连续空间
逻辑顺序 由物理位置决定 由引用链接决定
元素访问 支持随机访问(索引 O (1)) 不支持(需遍历 O (n))
插入 / 删除(任意位置) 需搬移元素(O (n)) 无需搬移(修改引用 O (1))
容量管理 固定容量,需扩容 无固定容量,动态增减节点
2.1.3 链表的节点结构

链表的基本组成单位是 "节点"(Node),每个节点通常包含两部分:

  1. 数据域(data):存储节点数据;
  2. 引用域(next/pre):存储指向其他节点的引用(指针)。
  • 单向链表节点结构:
java 复制代码
class Node {
    int data; // 数据域
    Node next; // 引用域(指向后一个节点)
    public Node(int data) {
        this.data = data;
        this.next = null;
    }
}
  • 双向链表节点结构:
java 复制代码
class Node {
    int data; // 数据域
    Node prev; // 引用域(指向前一个节点)
    Node next; // 引用域(指向后一个节点)
    public Node(int data) {
        this.data = data;
        this.prev = null;
        this.next = null;
    }
}

2.2 链表的分类:8 种组合结构

链表结构灵活,根据 3 个核心标准可组合出 8 种结构:

  1. 按节点链接方向:单向、双向;
  2. 按是否有头节点:带头、不带头;
  3. 按是否循环:循环、非循环。
2.2.1 分类详解
  1. 单向 vs 双向

    • 单向链表:每个节点仅一个 next 引用,只能从前往后遍历;
    • 双向链表:每个节点有 prevnext 两个引用,可双向遍历。
  2. 带头 vs 不带头

    • 带头链表:存在专门的 "头节点"(head),不存储实际数据,仅用于指向第一个数据节点,简化操作;
    • 不带头链表:无专门头节点,第一个节点即为数据节点,直接通过其引用访问链表。
  3. 循环 vs 非循环

    • 循环链表:最后一个节点的引用指向头节点(或第一个数据节点),形成闭环,遍历可循环;
    • 非循环链表:最后一个节点的引用为 null,遍历到尾节点结束。
2.2.2 8 种组合结构
  1. 单向、带头、循环;
  2. 单向、带头、非循环;
  3. 单向、不带头、循环;
  4. 单向、不带头、非循环;
  5. 双向、带头、循环;
  6. 双向、带头、非循环;
  7. 双向、不带头、循环;
  8. 双向、不带头、非循环。

2.3 重点掌握的两种链表结构

实际开发和面试中,重点掌握以下两种核心结构:

2.3.1 无头单向非循环链表
  • 结构特点:不带头节点、单向链接、非循环(尾节点 nextnull);
  • 优势:结构最简单,实现成本低;
  • 适用场景:极少单独存储数据,多作为其他数据结构的子结构(如哈希桶、图的邻接表),是笔试面试高频考点。

示意图:

java 复制代码
data1 -> data2 -> data3 -> data4 -> null
2.3.2 无头双向循环链表
  • 结构特点:不带头节点、双向链接、循环(尾节点 next 指向首节点,首节点 prev 指向尾节点);
  • 优势:支持双向遍历,插入 / 删除效率更高(无需遍历查找前驱节点);
  • 适用场景:Java 集合框架中 LinkedList 的底层实现。

示意图:

java 复制代码
data1 <-> data2 <-> data3 <-> data4
 ^                          |
 |                          v
 +--------------------------+

2.4 链表的核心操作思想

链表的核心操作(插入、删除、查找、遍历)逻辑与数组完全不同,核心是修改节点引用关系,而非搬移元素。

2.4.1 插入操作(单向链表示例)

在节点 A 和 B 之间插入节点 C:

  1. 找到前驱节点 A;
  2. C 的 next 指向 B;
  3. A 的 next 指向 C。

示意图:

java 复制代码
插入前:A -> B
插入后:A -> C -> B

双向链表插入需额外处理 prev 引用:

  1. 找到 A(前驱)和 B(后继);
  2. C 的 prev = A,C 的 next = B
  3. A 的 next = C,B 的 prev = C
2.4.2 删除操作(单向链表示例)

删除节点 B(前驱 A,后继 C):

  1. 找到前驱节点 A;
  2. A 的 next 直接指向 C;
  3. (Java 中)无需手动释放 B 的内存,垃圾回收机制自动处理。

示意图:

java 复制代码
删除前:A -> B -> C
删除后:A -> C

双向链表删除需同时处理 prevnext

  1. 找到 A(前驱)和 C(后继);
  2. A 的 next = C
  3. C 的 prev = A
2.4.3 查找操作

链表不支持随机访问,查找需遍历

  1. 从首节点开始;
  2. 依次对比每个节点的 data 与目标值;
  3. 找到返回节点,未找到返回 null

时间复杂度:O (n)(最坏需遍历所有节点)。

三、无头单向非循环链表的完整实现

无头单向非循环链表是链表的基础,也是面试高频考点。本节将实现其核心操作:头插、尾插、任意位置插入、查找、删除(单个 / 所有目标节点)、清空、遍历等。

3.1 节点类定义

java 复制代码
class Node {
    int data; // 数据域
    Node next; // 引用域(指向后一个节点)

    // 构造方法:初始化数据
    public Node(int data) {
        this.data = data;
        this.next = null; // 初始 next 为 null
    }
}

3.2 链表核心操作实现

java 复制代码
public class SingleLinkedList {
    // 链表头引用(指向第一个数据节点,无头节点)
    private Node head;
    // 有效元素个数
    private int size;

    // 构造方法:初始化空链表
    public SingleLinkedList() {
        this.head = null;
        this.size = 0;
    }

    // 1. 头插法:在链表头部插入元素
    public void addFirst(int data) {
        Node newNode = new Node(data);
        // 新节点 next 指向原头节点
        newNode.next = head;
        // 更新 head 为新节点
        head = newNode;
        size++;
    }

    // 2. 尾插法:在链表尾部插入元素
    public void addLast(int data) {
        Node newNode = new Node(data);
        // 空链表直接让 head 指向新节点
        if (head == null) {
            head = newNode;
        } else {
            // 遍历找到尾节点(next 为 null)
            Node cur = head;
            while (cur.next != null) {
                cur = cur.next;
            }
            // 尾节点 next 指向新节点
            cur.next = newNode;
        }
        size++;
    }

    // 3. 任意位置插入:第一个数据节点为 0 号下标
    public void addIndex(int index, int data) {
        // 校验 index 合法性(0 <= index <= size)
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("Index is illegal: " + index);
        }
        // index=0 直接头插
        if (index == 0) {
            addFirst(data);
            return;
        }
        // index=size 直接尾插
        if (index == size) {
            addLast(data);
            return;
        }
        // 找到 index 的前驱节点(index-1 位置)
        Node prev = findNodeByIndex(index - 1);
        // 创建新节点并修改引用
        Node newNode = new Node(data);
        newNode.next = prev.next;
        prev.next = newNode;
        size++;
    }

    // 辅助方法:根据索引查找节点(内部使用)
    private Node findNodeByIndex(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Index is illegal: " + index);
        }
        Node cur = head;
        // 遍历 index 次找到目标节点
        for (int i = 0; i < index; i++) {
            cur = cur.next;
        }
        return cur;
    }

    // 4. 查找是否包含关键字 key
    public boolean contains(int key) {
        Node cur = head;
        while (cur != null) {
            if (cur.data == key) {
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

    // 5. 删除第一次出现的关键字 key
    public void remove(int key) {
        // 空链表直接返回
        if (head == null) {
            return;
        }
        // 头节点为目标节点
        if (head.data == key) {
            head = head.next;
            size--;
            return;
        }
        // 找到目标节点的前驱
        Node prev = head;
        while (prev.next != null) {
            if (prev.next.data == key) {
                // 跳过目标节点(删除)
                prev.next = prev.next.next;
                size--;
                return;
            }
            prev = prev.next;
        }
        // 未找到 key 不操作
    }

    // 6. 删除所有值为 key 的节点
    public void removeAllKey(int key) {
        if (head == null) {
            return;
        }
        // 先处理中间和尾节点(跳过头节点)
        Node prev = head;
        Node cur = head.next;
        while (cur != null) {
            if (cur.data == key) {
                prev.next = cur.next;
                cur = cur.next;
                size--;
            } else {
                prev = cur;
                cur = cur.next;
            }
        }
        // 最后处理头节点(若头节点为 key)
        if (head.data == key) {
            head = head.next;
            size--;
        }
    }

    // 7. 获取链表长度
    public int size() {
        return size;
    }

    // 8. 遍历打印链表
    public void display() {
        Node cur = head;
        while (cur != null) {
            System.out.print(cur.data + " ");
            cur = cur.next;
        }
        System.out.println();
    }

    // 9. 清空链表
    public void clear() {
        // 方式 1:直接置空 head,GC 回收所有节点
        head = null;
        size = 0;

        // 方式 2:遍历断开所有引用(帮助 GC 快速回收)
        /*
        Node cur = head;
        while (cur != null) {
            Node next = cur.next;
            cur.next = null;
            cur = next;
        }
        head = null;
        size = 0;
        */
    }
}

3.3 测试代码

java 复制代码
public class SingleLinkedListTest {
    public static void main(String[] args) {
        SingleLinkedList list = new SingleLinkedList();

        // 测试尾插
        list.addLast(1);
        list.addLast(2);
        list.addLast(3);
        list.addLast(4);
        System.out.println("尾插后:");
        list.display(); // 输出:1 2 3 4
        System.out.println("长度:" + list.size()); // 4

        // 测试头插
        list.addFirst(0);
        System.out.println("头插后:");
        list.display(); // 0 1 2 3 4
        System.out.println("长度:" + list.size()); // 5

        // 测试任意位置插入
        list.addIndex(3, 99);
        System.out.println("索引 3 插入 99 后:");
        list.display(); // 0 1 2 99 3 4
        System.out.println("长度:" + list.size()); // 6

        // 测试 contains
        System.out.println("包含 99?" + list.contains(99)); // true
        System.out.println("包含 100?" + list.contains(100)); // false

        // 测试 remove(删除第一个 3)
        list.remove(3);
        System.out.println("删除第一个 3 后:");
        list.display(); // 0 1 2 99 4
        System.out.println("长度:" + list.size()); // 5

        // 测试 removeAllKey(添加重复 4)
        list.addLast(4);
        list.addLast(4);
        System.out.println("添加重复 4 后:");
        list.display(); // 0 1 2 99 4 4 4
        list.removeAllKey(4);
        System.out.println("删除所有 4 后:");
        list.display(); // 0 1 2 99
        System.out.println("长度:" + list.size()); // 4

        // 测试清空
        list.clear();
        System.out.println("清空后长度:" + list.size()); // 0
        list.display(); // 无输出
    }
}

3.4 实现要点

  1. 空链表判断 :所有操作前需判断 head == null,避免空指针;
  2. 索引合法性:插入、查找时校验索引范围,非法则抛异常;
  3. 头节点特殊处理 :头插、删除头节点直接修改 head 引用;
  4. 删除所有节点:先处理中间节点,最后处理头节点,避免漏删;
  5. 内存释放:Java 无需手动释放节点,断开引用后 GC 自动回收。

四、链表经典面试题解析(10 道核心题)

链表是笔试面试高频考点,以下 10 道题涵盖核心算法思想(快慢指针、虚拟头节点、链表拼接等),每道题含思路、代码、注意事项。

4.1 题目 1:删除链表中等于给定值 val 的所有节点(LeetCode 203)

题目描述

给链表头节点 head 和整数 val,删除所有 Node.val == val 的节点,返回新头节点。

示例

输入:head = [1,2,6,3,4,5,6], val = 6 → 输出:[1,2,3,4,5]

思路
  • 难点:头节点可能是目标节点,空链表需特殊处理;
  • 解决方案:虚拟头节点 (dummy node),其 next 指向原头节点,统一头节点与非头节点的删除逻辑;
  • 步骤:
    1. 创建虚拟头节点 dummydummy.next = head
    2. 定义 prev = dummy(前驱)、cur = head(当前);
    3. 遍历链表,cur.val == valprev.next = cur.next,否则 prev = cur
    4. cur = cur.next,遍历结束返回 dummy.next
代码实现
java 复制代码
public ListNode removeElements(ListNode head, int val) {
    // 虚拟头节点
    ListNode dummy = new ListNode(-1);
    dummy.next = head;
    ListNode prev = dummy;
    ListNode cur = head;

    while (cur != null) {
        if (cur.val == val) {
            prev.next = cur.next;
        } else {
            prev = cur;
        }
        cur = cur.next;
    }
    return dummy.next;
}

// 链表节点类(LeetCode 已定义)
class ListNode {
    int val;
    ListNode next;
    ListNode() {}
    ListNode(int val) { this.val = val; }
    ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
注意事项
  • 虚拟头节点统一删除逻辑,避免单独处理头节点;
  • 仅当当前节点不删除时,前驱节点才后移。

4.2 题目 2:反转一个单链表(LeetCode 206)

题目描述

给单链表头节点 head,反转链表并返回新头节点。

示例

输入:[1,2,3,4,5] → 输出:[5,4,3,2,1]

思路(迭代法)
  • 核心:双指针 ,修改节点 next 引用方向;
  • 步骤:
    1. prev = null(前驱),cur = head(当前);
    2. 遍历中保存 cur.next(避免反转后丢失);
    3. cur.next = prev(反转引用);
    4. prev = curcur = nextNode(指针后移);
    5. 遍历结束,prev 为新头节点。
代码实现(迭代法)
java 复制代码
public ListNode reverseList(ListNode head) {
    ListNode prev = null;
    ListNode cur = head;
    while (cur != null) {
        ListNode nextNode = cur.next; // 保存下一个节点
        cur.next = prev; // 反转
        prev = cur;
        cur = nextNode;
    }
    return prev;
}
进阶:递归法
java 复制代码
public ListNode reverseList(ListNode head) {
    // 终止条件:空链表或单个节点
    if (head == null || head.next == null) {
        return head;
    }
    // 递归反转后续链表
    ListNode newHead = reverseList(head.next);
    // 反转当前节点与下一个节点
    head.next.next = head;
    head.next = null; // 避免循环
    return newHead;
}
注意事项
  • 迭代法时间复杂度 O (n),空间复杂度 O (1)(原地反转);
  • 递归法时间复杂度 O (n),空间复杂度 O (n)(递归栈深度);
  • 必须先保存 cur.next,否则丢失后续链表。

4.3 题目 3:链表的中间结点(LeetCode 876)

题目描述

给非空单链表,返回中间节点;若有两个中间节点,返回第二个。

示例

输入:[1,2,3,4,5] → 输出:3;输入:[1,2,3,4,5,6] → 输出:4

思路
  • 核心:快慢指针,快指针每次走 2 步,慢指针每次走 1 步;
  • 原理:快指针遍历到尾时,慢指针恰好到中间:
    • 奇数长度:快指针到尾节点(fast.next == null),慢指针在中间;
    • 偶数长度:快指针到 null,慢指针在第二个中间节点。
代码实现
java 复制代码
public ListNode middleNode(ListNode head) {
    ListNode slow = head;
    ListNode fast = head;
    // 循环条件:fast 不为 null 且 fast.next 不为 null
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow;
}
注意事项
  • 循环条件不能写反(先判断 fast != null),避免空指针;
  • 时间复杂度 O (n),空间复杂度 O (1)。

4.4 题目 4:链表中倒数第 k 个结点(剑指 Offer 22)

题目描述

输入链表,输出倒数第 k 个节点(从 1 计数,尾节点为倒数第 1 个)。

示例

输入:1→2→3→4→5, k=2 → 输出:4

思路
  • 核心:快慢指针,快指针先出发 k 步,再与慢指针同时遍历,快指针到尾时,慢指针即为目标节点;
  • 步骤:
    1. 校验:链表为空或 k≤0 返回 null;
    2. 快指针先遍历 k 步(若过程中 fast == null,说明 k 大于链表长度,返回 null);
    3. 快慢指针同时遍历,直到 fast == null
    4. 慢指针即为倒数第 k 个节点。
代码实现
java 复制代码
public ListNode getKthFromEnd(ListNode head, int k) {
    if (head == null || k <= 0) {
        return null;
    }
    ListNode fast = head;
    // 快指针先跑 k 步
    for (int i = 0; i < k; i++) {
        if (fast == null) {
            return null; // k 超出链表长度
        }
        fast = fast.next;
    }
    // 快慢指针同时跑
    ListNode slow = head;
    while (fast != null) {
        slow = slow.next;
        fast = fast.next;
    }
    return slow;
}
注意事项
  • 需校验 k 的合法性和链表长度,避免数组越界;
  • 快慢指针间距为 k,快指针到尾时,慢指针恰好指向目标。

4.5 题目 5:合并两个有序链表(LeetCode 21)

题目描述

将两个升序链表合并为新的升序链表,返回新头节点。

示例

输入:l1 = [1,2,4], l2 = [1,3,4] → 输出:[1,1,2,3,4,4]

思路
  • 核心:双指针 + 虚拟头节点,类似归并排序的合并过程;
  • 步骤:
    1. 创建虚拟头节点 dummycurrent 指向 dummy
    2. 双指针 p1p2 分别指向两个链表头;
    3. 比较 p1p2 的值,将较小节点接入 current.next,对应指针后移;
    4. 遍历结束后,接入剩余链表;
    5. 返回 dummy.next
代码实现
java 复制代码
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
    ListNode dummy = new ListNode(-1);
    ListNode current = dummy;
    ListNode p1 = list1;
    ListNode p2 = list2;

    while (p1 != null && p2 != null) {
        if (p1.val <= p2.val) {
            current.next = p1;
            p1 = p1.next;
        } else {
            current.next = p2;
            p2 = p2.next;
        }
        current = current.next;
    }

    // 接入剩余节点
    current.next = (p1 != null) ? p1 : p2;
    return dummy.next;
}
进阶:递归法
java 复制代码
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
    if (list1 == null) return list2;
    if (list2 == null) return list1;
    if (list1.val <= list2.val) {
        list1.next = mergeTwoLists(list1.next, list2);
        return list1;
    } else {
        list2.next = mergeTwoLists(list1, list2.next);
        return list2;
    }
}
注意事项
  • 迭代法时间复杂度 O (n+m)(n、m 为链表长度),空间复杂度 O (1);
  • 递归法时间复杂度 O (n+m),空间复杂度 O (n+m)(递归栈)。

4.6 题目 6:以给定值 x 为基准分割链表(LeetCode 86)

题目描述

给链表头节点 head 和值 x,分割链表使所有小于 x 的节点在前,大于等于 x 的节点在后,保留节点初始相对位置。

示例

输入:[1,4,3,2,5,2], x=3 → 输出:[1,2,2,4,3,5]

思路
  • 核心:拆分链表 + 拼接 ,创建两个虚拟头节点分别存储小于 x 和大于等于 x 的节点,最后拼接;
  • 步骤:
    1. 创建 smallDummy(存小于 x)、largeDummy(存大于等于 x);
    2. smalllarge 指针分别指向两个虚拟头节点;
    3. 遍历原链表,分配节点到对应链表;
    4. small.next = largeDummy.next(拼接);
    5. large.next = null(避免循环);
    6. 返回 smallDummy.next
代码实现
java 复制代码
public ListNode partition(ListNode head, int x) {
    ListNode smallDummy = new ListNode(-1);
    ListNode largeDummy = new ListNode(-1);
    ListNode small = smallDummy;
    ListNode large = largeDummy;
    ListNode cur = head;

    while (cur != null) {
        if (cur.val < x) {
            small.next = cur;
            small = small.next;
        } else {
            large.next = cur;
            large = large.next;
        }
        cur = cur.next;
    }

    // 拼接两个链表
    small.next = largeDummy.next;
    large.next = null; // 避免循环
    return smallDummy.next;
}
注意事项
  • 必须将 large.next 置为 null,否则可能出现链表循环;
  • 时间复杂度 O (n),空间复杂度 O (1)。

4.7 题目 7:链表的回文结构(剑指 Offer 27)

题目描述

判断链表是否为回文链表(正序与逆序遍历结果一致)。

示例

输入:1→2→2→1 → 输出:true;输入:1→2 → 输出:false

思路
  • 核心:快慢指针找中间 + 反转后半部分 + 对比前后
  • 步骤:
    1. 快慢指针找中间节点;
    2. 反转后半部分链表;
    3. 双指针分别从表头和反转后的后半部分表头对比;
    4. (可选)恢复原链表结构;
    5. 所有节点相等则为回文。
代码实现
java 复制代码
public boolean isPalindrome(ListNode head) {
    if (head == null || head.next == null) {
        return true;
    }

    // 步骤 1:找中间节点
    ListNode slow = head;
    ListNode fast = head;
    while (fast.next != null && fast.next.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    ListNode secondHalfHead = slow.next; // 后半部分头节点

    // 步骤 2:反转后半部分
    ListNode reversedSecondHalf = reverseList(secondHalfHead);

    // 步骤 3:对比前后两部分
    ListNode p1 = head;
    ListNode p2 = reversedSecondHalf;
    boolean isPalindrome = true;
    while (p2 != null) {
        if (p1.val != p2.val) {
            isPalindrome = false;
            break;
        }
        p1 = p1.next;
        p2 = p2.next;
    }

    // 步骤 4:恢复原链表(可选)
    slow.next = reverseList(reversedSecondHalf);

    return isPalindrome;
}

// 辅助方法:反转链表(同题目 2)
private ListNode reverseList(ListNode head) {
    ListNode prev = null;
    ListNode cur = head;
    while (cur != null) {
        ListNode nextNode = cur.next;
        cur.next = prev;
        prev = cur;
        cur = nextNode;
    }
    return prev;
}
注意事项
  • 找中间节点时,循环条件确保慢指针指向中间(偶数长度指向第一个中间节点);
  • 对比时仅需遍历到 p2null(后半部分长度 ≤ 前半部分);
  • 恢复原链表是良好编程习惯,避免修改输入数据。

4.8 题目 8:两个链表的第一个公共节点(剑指 Offer 52)

题目描述

输入两个链表,找出第一个公共节点(物理地址相同,非仅值相等)。

示例

输入:链表 A 4→1→8→4→5,链表 B 5→0→1→8→4→5 → 输出:8

思路
  • 核心:双指针相遇法,两个指针分别从两个链表头出发,遍历到尾后切换到另一个链表头,直到相遇;
  • 原理:设链表 A 长度 m,B 长度 n,公共部分长度 k,则非公共部分长度为 m-k 和 n-k;两指针总遍历长度均为 m+n,最终会在公共节点相遇(无公共节点则都到 null)。
代码实现
java 复制代码
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    if (headA == null || headB == null) {
        return null;
    }
    ListNode pA = headA;
    ListNode pB = headB;

    while (pA != pB) {
        // 遍历完一个链表则切换到另一个
        pA = (pA == null) ? headB : pA.next;
        pB = (pB == null) ? headA : pB.next;
    }
    return pA;
}
注意事项
  • 公共节点是物理地址相同,而非值相等;
  • 时间复杂度 O (m+n),空间复杂度 O (1);
  • 无公共节点时,两指针最终都为 null,循环退出返回 null

4.9 题目 9:判断链表中是否有环(LeetCode 141)

题目描述

判断链表是否有环(存在节点可通过 next 指针再次到达)。

示例

输入:有环链表 3→2→0→-4→2 → 输出:true;输入:无环链表 1→2 → 输出:false

思路
  • 核心:快慢指针,慢指针走 1 步,快指针走 2 步;
  • 原理:
    • 无环:快指针先到 null,返回 false;
    • 有环:快指针和慢指针最终在环中相遇(每次距离缩小 1 步,不会套圈)。
关键问题解答
  1. 为什么快指针走 2 步、慢指针走 1 步能相遇?

    • 慢指针进环时,快指针已在环中,两者最大距离为环长 R;
    • 每次距离缩小 1 步,慢指针走 1 圈前必被追上。
  2. 快指针走 3 步、4 步可行吗?

    • 不可行。例如快指针走 3 步、慢指针走 1 步,可能出现快指针跳过慢指针,永远不相遇。
代码实现
java 复制代码
public boolean hasCycle(ListNode head) {
    if (head == null || head.next == null) {
        return false;
    }
    ListNode slow = head;
    ListNode fast = head.next; // 初始位置避免循环未开始就相等

    while (slow != fast) {
        if (fast == null || fast.next == null) {
            return false;
        }
        slow = slow.next;
        fast = fast.next.next;
    }
    return true;
}
注意事项
  • 快慢指针初始位置可设为(head, head)或(head, head.next),前者循环条件为 fast != null && fast.next != null
  • 有环必相遇,无环快指针先到 null,不会死循环;
  • 时间复杂度 O (n),空间复杂度 O (1)。

4.10 题目 10:链表入环的第一个节点(LeetCode 142)

题目描述

给链表头节点 head,返回入环第一个节点;无环返回 null

示例

输入:有环链表 3→2→0→-4→2 → 输出:2

思路
  • 核心:快慢指针相遇 + 双指针相遇,结合数学推导;

  • 数学推导:定义:

    • H:链表起始点;E:入环点;M:快慢指针相遇点;
    • L:H 到 E 的距离;X:E 到 M 的距离;R:环长。

    推导:

    1. 相遇时,慢指针走了 L+X,快指针走了 L+X+nR(n≥1);
    2. 快指针速度是慢指针 2 倍:2 (L+X) = L+X+nR → L = nR - X;
    3. 结论:从 H 和 M 出发的指针,每次走 1 步,最终在 E 相遇。
  • 步骤:

    1. 快慢指针判断是否有环,找到相遇点 M;
    2. 双指针分别从 H(head)和 M 出发,相遇点即为 E。
代码实现
java 复制代码
public ListNode detectCycle(ListNode head) {
    ListNode slow = head;
    ListNode fast = head;
    boolean hasCycle = false;

    // 步骤 1:判断是否有环,找到相遇点
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) {
            hasCycle = true;
            break;
        }
    }

    if (!hasCycle) {
        return null;
    }

    // 步骤 2:双指针找入环点
    ListNode p1 = head;
    ListNode p2 = slow;
    while (p1 != p2) {
        p1 = p1.next;
        p2 = p2.next;
    }

    return p1;
}
注意事项
  • 数学推导是核心,需理解 L = nR - X 的含义;
  • 当 n=1 时,L = R - X,两指针同时到达 E;
  • 时间复杂度 O (n),空间复杂度 O (1),为最优解法。

五、无头双向循环链表的实现(LinkedList 底层模拟)

Java 中 LinkedList 底层是无头双向循环链表,支持双向遍历,插入 / 删除效率更高。本节模拟其核心操作,帮助理解 LinkedList 底层原理。

5.1 节点类定义

java 复制代码
class ListNode {
    int data; // 数据域
    ListNode prev; // 前驱引用
    ListNode next; // 后继引用

    public ListNode(int data) {
        this.data = data;
        this.prev = null;
        this.next = null;
    }
}

5.2 链表核心操作实现

java 复制代码
public class MyLinkedList {
    private ListNode head; // 头引用(指向第一个数据节点)
    private int size; // 有效元素个数

    public MyLinkedList() {
        this.head = null;
        this.size = 0;
    }

    // 1. 头插法
    public void addFirst(int data) {
        ListNode newNode = new ListNode(data);
        if (head == null) {
            // 空链表,新节点自循环
            head = newNode;
            newNode.prev = newNode;
            newNode.next = newNode;
        } else {
            ListNode last = head.prev; // 原尾节点(head 的前驱)
            // 新节点的前驱指向尾节点,后继指向头节点
            newNode.prev = last;
            newNode.next = head;
            // 尾节点的后继指向新节点,头节点的前驱指向新节点
            last.next = newNode;
            head.prev = newNode;
            // 更新 head 为新节点
            head = newNode;
        }
        size++;
    }

    // 2. 尾插法
    public void addLast(int data) {
        ListNode newNode = new ListNode(data);
        if (head == null) {
            addFirst(data);
            return;
        }
        ListNode last = head.prev; // 原尾节点
        // 新节点前驱指向尾节点,后继指向头节点(循环)
        newNode.prev = last;
        newNode.next = head;
        // 尾节点后继指向新节点,头节点前驱指向新节点
        last.next = newNode;
        head.prev = newNode;
        size++;
    }

    // 3. 任意位置插入
    public void addIndex(int index, int data) {
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("Index is illegal: " + index);
        }
        if (index == 0) {
            addFirst(data);
            return;
        }
        if (index == size) {
            addLast(data);
            return;
        }
        // 找到 index 位置的节点
        ListNode cur = findNodeByIndex(index);
        ListNode prevNode = cur.prev; // 前驱节点
        // 创建新节点并修改引用
        ListNode newNode = new ListNode(data);
        newNode.prev = prevNode;
        newNode.next = cur;
        prevNode.next = newNode;
        cur.prev = newNode;
        size++;
    }

    // 辅助方法:根据索引查找节点(优化遍历方向)
    private ListNode findNodeByIndex(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Index is illegal: " + index);
        }
        ListNode cur = head;
        // 前半部分:从 head 向后遍历
        if (index < size / 2) {
            for (int i = 0; i < index; i++) {
                cur = cur.next;
            }
        } else {
            // 后半部分:从尾节点向前遍历
            for (int i = 0; i < size - index; i++) {
                cur = cur.prev;
            }
        }
        return cur;
    }

    // 4. 查找是否包含 key
    public boolean contains(int key) {
        if (head == null) {
            return false;
        }
        ListNode cur = head;
        // 循环条件:cur != head(双向循环)
        do {
            if (cur.data == key) {
                return true;
            }
            cur = cur.next;
        } while (cur != head);
        return false;
    }

    // 5. 删除第一次出现的 key
    public void remove(int key) {
        if (head == null) {
            return;
        }
        ListNode cur = head;
        do {
            if (cur.data == key) {
                // 链表只有一个节点
                if (size == 1) {
                    head = null;
                } else {
                    // 删除头节点
                    if (cur == head) {
                        head = head.next;
                    }
                    // 修改前驱和后继引用
                    ListNode prevNode = cur.prev;
                    ListNode nextNode = cur.next;
                    prevNode.next = nextNode;
                    nextNode.prev = prevNode;
                }
                size--;
                return;
            }
            cur = cur.next;
        } while (cur != head);
    }

    // 6. 删除所有值为 key 的节点
    public void removeAllKey(int key) {
        if (head == null) {
            return;
        }
        ListNode cur = head;
        do {
            ListNode nextNode = cur.next; // 保存下一个节点
            if (cur.data == key) {
                if (size == 1) {
                    head = null;
                } else {
                    if (cur == head) {
                        head = nextNode;
                    }
                    // 修改引用
                    cur.prev.next = nextNode;
                    nextNode.prev = cur.prev;
                }
                size--;
            }
            cur = nextNode;
        } while (head != null && cur != head);
    }

    // 7. 获取链表长度
    public int size() {
        return size;
    }

    // 8. 正向遍历打印
    public void display() {
        if (head == null) {
            System.out.println("链表为空");
            return;
        }
        ListNode cur = head;
        do {
            System.out.print(cur.data + " ");
            cur = cur.next;
        } while (cur != head);
        System.out.println();
    }

    // 9. 反向遍历打印
    public void displayReverse() {
        if (head == null) {
            System.out.println("链表为空");
            return;
        }
        ListNode cur = head.prev; // 从尾节点开始
        do {
            System.out.print(cur.data + " ");
            cur = cur.prev;
        } while (cur != head.prev);
        System.out.println();
    }

    // 10. 清空链表
    public void clear() {
        if (head == null) {
            return;
        }
        ListNode cur = head;
        do {
            ListNode nextNode = cur.next;
            // 断开所有引用
            cur.prev = null;
            cur.next = null;
            cur = nextNode;
        } while (cur != head);
        head = null;
        size = 0;
    }
}

5.3 测试代码

java 复制代码
public class MyLinkedListTest {
    public static void main(String[] args) {
        MyLinkedList list = new MyLinkedList();

        // 尾插测试
        list.addLast(1);
        list.addLast(2);
        list.addLast(3);
        list.addLast(4);
        System.out.println("尾插正向遍历:");
        list.display(); // 1 2 3 4
        System.out.println("尾插反向遍历:");
        list.displayReverse(); // 4 3 2 1
        System.out.println("长度:" + list.size()); // 4

        // 头插测试
        list.addFirst(0);
        System.out.println("头插正向遍历:");
        list.display(); // 0 1 2 3 4
        System.out.println("长度:" + list.size()); // 5

        // 任意位置插入测试
        list.addIndex(3, 99);
        System.out.println("索引 3 插入 99 后:");
        list.display(); // 0 1 2 99 3 4
        System.out.println("长度:" + list.size()); // 6

        // contains 测试
        System.out.println("包含 99?" + list.contains(99)); // true
        System.out.println("包含 100?" + list.contains(100)); // false

        // remove 测试
        list.remove(3);
        System.out.println("删除第一个 3 后:");
        list.display(); // 0 1 2 99 4
        System.out.println("长度:" + list.size()); // 5

        // removeAllKey 测试
        list.addLast(4);
        list.addLast(4);
        System.out.println("添加重复 4 后:");
        list.display(); // 0 1 2 99 4 4 4
        list.removeAllKey(4);
        System.out.println("删除所有 4 后:");
        list.display(); // 0 1 2 99
        System.out.println("长度:" + list.size()); // 4

        // 清空测试
        list.clear();
        System.out.println("清空后长度:" + list.size()); // 0
        list.display(); // 链表为空
    }
}

5.4 双向循环链表的优势与实现要点

优势
  1. 支持双向遍历,灵活度高;
  2. 插入 / 删除效率高(O (1)),无需查找前驱节点;
  3. 循环结构避免 null 判断,遍历逻辑简洁。
实现要点
  1. 维护循环结构:插入 / 删除时确保尾节点 next 指向头节点,头节点 prev 指向尾节点;
  2. 头节点更新:头插或删除头节点时及时更新 head
  3. 遍历终止条件:cur != head(而非 cur == null);
  4. 索引查找优化:根据索引位置选择正向或反向遍历,减少次数。

六、LinkedList 的使用指南(Java 集合框架)

Java 集合框架中的 LinkedList 基于无头双向循环链表实现,实现了 List、Deque 等接口,提供丰富 API。本节详细介绍其使用方法。

6.1 LinkedList 的类结构与核心特性

类结构
java 复制代码
java.lang.Object
  ↳ java.util.AbstractCollection<E>
    ↳ java.util.AbstractList<E>
      ↳ java.util.AbstractSequentialList<E>
        ↳ java.util.LinkedList<E>

实现的核心接口:

  • List<E>:提供有序、可重复、支持索引的功能;
  • Deque<E>:提供双端队列功能(两端插入 / 删除);
  • Cloneable:支持浅克隆;
  • Serializable:支持序列化。
核心特性
  1. 底层:无头双向循环链表;
  2. 随机访问:不支持(未实现 RandomAccess 接口),索引访问需遍历(O (n));
  3. 插入 / 删除:任意位置 O (1)(查找位置 O (n),修改引用 O (1));
  4. 容量:无固定容量,动态增减节点,无需扩容;
  5. 线程安全:非线程安全,多线程需手动同步(如 Collections.synchronizedList());
  6. 支持 null 元素。

6.2 LinkedList 的构造方法

构造方法 说明
LinkedList() 无参构造,创建空链表
LinkedList(Collection<? extends E> c) 有参构造,用集合 c 的元素初始化
使用示例
java 复制代码
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class LinkedListConstructor {
    public static void main(String[] args) {
        // 无参构造
        List<Integer> list1 = new LinkedList<>();
        list1.add(1);
        list1.add(2);
        System.out.println("无参构造:" + list1); // [1, 2]

        // 有参构造(用 ArrayList 初始化)
        List<String> arrayList = new ArrayList<>();
        arrayList.add("JavaSE");
        arrayList.add("JavaWeb");
        List<String> list2 = new LinkedList<>(arrayList);
        System.out.println("有参构造:" + list2); // [JavaSE, JavaWeb]
    }
}

6.3 LinkedList 的核心方法(List 接口)

方法 说明 时间复杂度
boolean add(E e) 尾插 e O(1)
void add(int index, E e) 索引 index 插入 e O (n)(查找 index O (n))
boolean addAll(Collection<? extends E> c) 尾插集合 c 的元素 O (m)(m 为集合大小)
E remove(int index) 删除 index 位置元素,返回该元素 O (n)(查找 index O (n))
boolean remove(Object o) 删除第一个等于 o 的元素 O (n)(查找 O (n))
E get(int index) 获取 index 位置元素 O (n)(查找 O (n))
E set(int index, E e) 修改 index 位置元素为 e,返回原元素 O (n)(查找 O (n))
boolean contains(Object o) 判断是否包含 o O(n)
int indexOf(Object o) 返回第一个 o 的索引,未找到返回 -1 O(n)
int lastIndexOf(Object o) 返回最后一个 o 的索引 O(n)
List<E> subList(int fromIndex, int toIndex) 返回 [fromIndex, toIndex) 子链表(视图) O(1)
void clear() 清空链表 O(n)
int size() 返回元素个数 O(1)
使用示例
java 复制代码
import java.util.LinkedList;
import java.util.List;

public class LinkedListListMethods {
    public static void main(String[] args) {
        LinkedList<Integer> list = new LinkedList<>();

        // 1. add(E e):尾插
        list.add(1);
        list.add(2);
        list.add(3);
        System.out.println("尾插后:" + list); // [1, 2, 3]

        // 2. add(int index, E e):任意位置插入
        list.add(1, 99);
        System.out.println("索引 1 插入 99 后:" + list); // [1, 99, 2, 3]

        // 3. addAll(Collection):尾插集合
        LinkedList<Integer> other = new LinkedList<>();
        other.add(4);
        other.add(5);
        list.addAll(other);
        System.out.println("尾插 other 后:" + list); // [1, 99, 2, 3, 4, 5]

        // 4. get(int index):获取元素
        int elem = list.get(3);
        System.out.println("索引 3 的元素:" + elem); // 3

        // 5. set(int index, E e):修改元素
        int oldElem = list.set(3, 100);
        System.out.println("修改索引 3:原元素=" + oldElem + ",链表=" + list); // [1, 99, 2, 100, 4, 5]

        // 6. contains(Object o):判断包含
        System.out.println("包含 99?" + list.contains(99)); // true

        // 7. indexOf/lastIndexOf
        list.add(99);
        System.out.println("第一个 99 的索引:" + list.indexOf(99)); // 1
        System.out.println("最后一个 99 的索引:" + list.lastIndexOf(99)); // 6

        // 8. subList:子链表(视图)
        List<Integer> sub = list.subList(1, 4);
        System.out.println("子链表 [1,4):" + sub); // [99, 2, 100]

        // 9. remove:删除
        list.remove(1); // 删除索引 1 的 99
        list.remove(Integer.valueOf(4)); // 删除元素 4
        System.out.println("删除后:" + list); // [1, 2, 100, 5, 99]

        // 10. clear:清空
        list.clear();
        System.out.println("清空后长度:" + list.size()); // 0
    }
}

6.4 LinkedList 的双端队列方法(Deque 接口)

LinkedList 实现 Deque 接口,支持双端队列操作(两端插入 / 删除),效率 O (1):

方法 说明
void addFirst(E e) 头插 e
void addLast(E e) 尾插 e(同 add (E e))
boolean offerFirst(E e) 头插 e,成功返回 true
boolean offerLast(E e) 尾插 e,成功返回 true
E removeFirst() 删除头节点,返回该元素(空链表抛异常)
E removeLast() 删除尾节点,返回该元素(空链表抛异常)
E pollFirst() 删除头节点,返回该元素(空链表返回 null)
E pollLast() 删除尾节点,返回该元素(空链表返回 null)
E getFirst() 获取头节点(空链表抛异常)
E getLast() 获取尾节点(空链表抛异常)
E peekFirst() 获取头节点(空链表返回 null)
E peekLast() 获取尾节点(空链表返回 null)
使用示例
java 复制代码
import java.util.LinkedList;
import java.util.Deque;

public class LinkedListDequeMethods {
    public static void main(String[] args) {
        Deque<Integer> deque = new LinkedList<>();

        // 头插/尾插
        deque.addFirst(1);
        deque.addLast(2);
        deque.offerFirst(0);
        deque.offerLast(3);
        System.out.println("双端操作后:" + deque); // [0, 1, 2, 3]

        // 获取头/尾节点
        System.out.println("头节点:" + deque.getFirst()); // 0
        System.out.println("尾节点:" + deque.peekLast()); // 3

        // 删除头/尾节点
        deque.removeFirst();
        deque.pollLast();
        System.out.println("删除后:" + deque); // [1, 2]
    }
}

6.5 LinkedList 的遍历方式

LinkedList 支持 3 种常用遍历方式:foreach、迭代器、普通 for 循环(不推荐)。

遍历示例
java 复制代码
import java.util.LinkedList;
import java.util.ListIterator;

public class LinkedListTraversal {
    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 遍历(推荐,简洁)
        System.out.println("foreach 遍历:");
        for (int e : list) {
            System.out.print(e + " "); // 1 2 3 4
        }
        System.out.println();

        // 2. 迭代器正向遍历
        System.out.println("迭代器正向遍历:");
        ListIterator<Integer> it = list.listIterator();
        while (it.hasNext()) {
            System.out.print(it.next() + " "); // 1 2 3 4
        }
        System.out.println();

        // 3. 迭代器反向遍历
        System.out.println("迭代器反向遍历:");
        ListIterator<Integer> rit = list.listIterator(list.size());
        while (rit.hasPrevious()) {
            System.out.print(rit.previous() + " "); // 4 3 2 1
        }
        System.out.println();

        // 4. 普通 for 循环(不推荐,每次 get 都遍历)
        System.out.println("普通 for 循环:");
        for (int i = 0; i < list.size(); i++) {
            System.out.print(list.get(i) + " "); // 1 2 3 4
        }
        System.out.println();
    }
}
注意事项
  • 普通 for 循环遍历效率低(每次 get(i) 需从表头 / 表尾遍历到索引 i);
  • 迭代器支持双向遍历,且可通过 addremove 方法修改链表(foreach 不支持)。

七、ArrayList 与 LinkedList 的核心区别与应用场景

7.1 核心区别对比

对比维度 ArrayList LinkedList
底层结构 动态数组(连续物理空间) 无头双向循环链表(非连续空间)
随机访问 支持(O (1)) 不支持(O (n))
头插 / 头删 需搬移元素(O (n)) 修改引用(O (1))
中间插入 / 删除 需搬移元素(O (n)) 查找位置(O (n))+ 修改引用(O (1))
尾插 / 尾删 无扩容时 O (1),需扩容时 O (n) O(1)
容量管理 固定容量,需扩容(1.5 倍) 无容量概念,动态增减节点
内存占用 连续空间,可能有冗余(扩容后未使用空间) 每个节点需存储 prev/next 引用,内存开销略大
线程安全 非线程安全 非线程安全
实现接口 List、RandomAccess 等 List、Deque、Cloneable 等

7.2 应用场景选择

优先使用 ArrayList 的场景
  1. 频繁通过索引访问元素(随机访问);
  2. 元素数量稳定,插入 / 删除操作少(尤其是中间插入 / 删除);
  3. 对内存连续性要求高,需减少额外内存开销。
优先使用 LinkedList 的场景
  1. 频繁进行任意位置插入 / 删除操作(尤其是头插 / 头删);
  2. 需实现双端队列功能(两端插入 / 删除);
  3. 元素数量动态变化大,无需考虑扩容。

7.3 常见误区澄清

  1. 误区 1:LinkedList 插入 / 删除一定比 ArrayList 快?

    • 澄清:LinkedList 插入 / 删除的时间复杂度是 "查找位置 O (n) + 修改引用 O (1)",若插入位置靠近两端,效率很高;但如果插入位置在中间(需遍历查找),效率可能不如 ArrayList(尤其是元素数量少时)。
  2. 误区 2:ArrayList 扩容一定影响性能?

    • 澄清:ArrayList 扩容频率低(初始容量 10,每次扩容 1.5 倍),若提前预估元素数量并指定初始容量(new ArrayList<>(1000)),可避免扩容,性能优异。
  3. 误区 3:LinkedList 支持随机访问?

    • 澄清:LinkedList 未实现 RandomAccess 接口,通过 get(i) 访问元素需遍历,不支持随机访问,遍历效率低。

八、总结与拓展

8.1 核心知识点总结

  1. 链表是物理非连续、逻辑连续的数据结构,核心是节点引用关系;
  2. 重点掌握两种链表结构:无头单向非循环链表(面试高频)、无头双向循环链表(LinkedList 底层);
  3. 链表面试题核心思想:快慢指针、虚拟头节点、链表拆分 / 拼接、递归;
  4. LinkedList 是 List 与 Deque 接口的实现类,适用于频繁插入 / 删除场景;
  5. ArrayList 与 LinkedList 的选择核心是 "随机访问" 与 "插入 / 删除" 的权衡。

8.2 拓展学习建议

  1. 刷题练习:LeetCode 链表专题(Easy~Medium 难度),重点练习本文 10 道题的变种;
  2. 源码阅读:阅读 JDK 中 LinkedList 源码,理解其底层实现细节(如节点类、循环结构维护);
  3. 进阶数据结构:学习基于链表的复杂数据结构(如哈希桶、图的邻接表、LinkedHashMap 底层);
  4. 线程安全:学习如何保证 LinkedList/ArrayList 线程安全(Collections.synchronizedList()CopyOnWriteArrayList 等)。

通过本文的学习,相信大家已全面掌握链表与 LinkedList 的核心知识。链表作为基础数据结构,是算法学习的重中之重,建议多动手实现、多刷题巩固,将知识转化为实战能力。


总结

以上就是今天要讲的内容,本文简单记录了java数据结构,仅作为一份简单的笔记使用,大家根据注释理解,您的点赞关注收藏就是对小编最大的鼓励!

相关推荐
m0_706653231 小时前
用Python批量处理Excel和CSV文件
jvm·数据库·python
山岚的运维笔记1 小时前
SQL Server笔记 -- 第15章:INSERT INTO
java·数据库·笔记·sql·microsoft·sqlserver
witAI2 小时前
**AI漫剧制作工具2025推荐,零成本实现专业级动画创作*
人工智能·python
千秋乐。2 小时前
C++-string
开发语言·c++
孞㐑¥2 小时前
算法—队列+宽搜(bfs)+堆
开发语言·c++·经验分享·笔记·算法
yufuu982 小时前
并行算法在STL中的应用
开发语言·c++·算法
charlie1145141912 小时前
嵌入式C++教程——ETL(Embedded Template Library)
开发语言·c++·笔记·学习·嵌入式·etl
陳10302 小时前
C++:AVL树的模拟实现
开发语言·c++
小王不爱笑1322 小时前
LangChain4J 整合多 AI 模型核心实现步骤
java·人工智能·spring boot