Java 链表完全指南:从基础到力扣简单题实战

在数据结构的学习中,链表与数组是两种最基础的线性结构,却有着截然不同的特性。相比于数组的连续内存存储,链表通过指针串联节点的灵活设计,在插入、删除操作中展现出独特优势。本文将系统梳理 Java 链表的核心知识,从节点定义到复杂操作,再到力扣简单题的解题技巧,帮助你构建完整的链表知识体系,轻松应对各类基础链表问题。

一、链表的本质与核心概念

1.1 什么是链表?

链表是一种通过节点串联形成的线性数据结构,每个节点包含两个关键部分:

  • 数据域:存储具体的数据(如整数、字符串、对象等)
  • 指针域:存储下一个(或上一个)节点的引用地址,实现节点间的逻辑关联

这种结构允许节点在内存中分散存储,无需占用连续的内存空间,理论上可以无限扩展(受限于设备内存)。与数组相比,链表的优势在于:

  • 不需要预先分配固定大小的内存,动态性更强
  • 插入和删除操作无需大规模移动元素,仅需修改指针指向
  • 适合频繁修改数据顺序的场景

1.2 常见链表类型

在 Java 中,我们主要接触以下三种链表类型,其中单向链表是力扣简单题的核心考查对象:

单向链表:最基础的链表结构,每个节点仅包含指向下一个节点的指针。

java 复制代码
class ListNode {
    int val;          // 数据域,存储节点值
    ListNode next;    // 指针域,指向下一个节点
    
    // 构造方法,初始化节点值
    ListNode(int x) {
        val = x;
        next = null;  // 初始化时默认指向null
    }
}

双向链表:每个节点同时包含前驱指针(prev)和后继指针(next),支持双向遍历,操作更灵活但结构更复杂。

java 复制代码
class DoubleListNode {
    int val;
    DoubleListNode prev;  // 指向前一个节点
    DoubleListNode next;  // 指向后一个节点
    
    DoubleListNode(int x) {
        val = x;
        prev = null;
        next = null;
    }
}

循环链表:尾节点的指针不指向 null,而是指向头节点,形成闭合环形结构,适合实现循环队列、约瑟夫问题等场景。

二、链表的核心操作实现

2.1 链表的创建与初始化

创建链表是所有操作的基础,常用的两种方式分别适用于不同场景:

手动构建固定链表:适合创建测试用例或已知数据的链表

java 复制代码
/**
 * 创建包含1→2→3→4→5的链表
 * @return 链表头节点
 */
public static ListNode createLinkedList() {
    // 1. 创建各个节点
    ListNode head = new ListNode(1);
    ListNode node2 = new ListNode(2);
    ListNode node3 = new ListNode(3);
    ListNode node4 = new ListNode(4);
    ListNode node5 = new ListNode(5);
    
    // 2. 建立节点间的连接
    head.next = node2;
    node2.next = node3;
    node3.next = node4;
    node4.next = node5;  // 尾节点next默认指向null
    
    return head;  // 返回头节点(整个链表的入口)
}

从数组动态创建链表:更灵活,可根据输入数据动态生成链表

java 复制代码
/**
 * 根据数组创建链表
 * @param arr 输入数组
 * @return 链表头节点
 */
public static ListNode createFromArray(int[] arr) {
    if (arr == null || arr.length == 0) {
        return null;  // 空数组返回空链表
    }
    
    // 创建头节点
    ListNode head = new ListNode(arr[0]);
    ListNode current = head;  // 临时指针,用于连接后续节点
    
    // 遍历数组剩余元素,创建节点并连接
    for (int i = 1; i < arr.length; i++) {
        current.next = new ListNode(arr[i]);
        current = current.next;  // 移动指针到新节点
    }
    
    return head;
}

2.2 链表的遍历操作

遍历是所有链表操作的基础,通过移动指针逐个访问节点:

java 复制代码
/**
 * 遍历链表并打印节点值
 * @param head 链表头节点
 */
public static void traverse(ListNode head) {
    ListNode current = head;  // 使用临时指针,避免修改头节点
    
    // 循环条件:当前节点不为null(未到达链表末尾)
    while (current != null) {
        System.out.print(current.val + "→");
        current = current.next;  // 移动到下一个节点
    }
    System.out.println("null");  // 标识链表结束
}

遍历操作的时间复杂度为 O (n)(需访问所有节点),空间复杂度为 O (1)(仅使用常数级额外空间)。

2.3 节点的插入操作

根据插入位置不同,分为头部插入、尾部插入和中间插入三种场景:

头部插入:新节点成为新的头节点

java 复制代码
/**
 * 在链表头部插入新节点
 * @param head 原链表头节点
 * @param val 新节点的值
 * @return 新的头节点
 */
public static ListNode insertAtHead(ListNode head, int val) {
    ListNode newNode = new ListNode(val);
    newNode.next = head;  // 新节点指向原头节点
    return newNode;       // 新节点成为新的头节点
}

尾部插入:新节点成为尾节点

java 复制代码
/**
 * 在链表尾部插入新节点
 * @param head 链表头节点
 * @param val 新节点的值
 * @return 原头节点(头部未改变)
 */
public static ListNode insertAtTail(ListNode head, int val) {
    ListNode newNode = new ListNode(val);
    
    // 特殊情况:链表为空时,新节点直接作为头节点
    if (head == null) {
        return newNode;
    }
    
    // 找到尾节点(next为null的节点)
    ListNode current = head;
    while (current.next != null) {
        current = current.next;
    }
    
    current.next = newNode;  // 尾节点指向新节点
    return head;
}

中间插入:在指定索引位置插入节点(索引从 0 开始)

java 复制代码
/**
 * 在指定索引位置插入节点
 * @param head 头节点
 * @param index 插入位置
 * @param val 节点值
 * @return 头节点
 */
public static ListNode insertAtIndex(ListNode head, int index, int val) {
    // 索引为0时,相当于头部插入
    if (index == 0) {
        return insertAtHead(head, val);
    }
    
    ListNode newNode = new ListNode(val);
    ListNode current = head;
    int currentIndex = 0;
    
    // 找到插入位置的前一个节点
    while (current != null && currentIndex < index - 1) {
        current = current.next;
        currentIndex++;
    }
    
    // 处理索引越界(如index超过链表长度)
    if (current == null) {
        throw new IndexOutOfBoundsException("插入索引超出链表长度");
    }
    
    // 插入新节点(先连后断,避免节点丢失)
    newNode.next = current.next;
    current.next = newNode;
    
    return head;
}

2.4 节点的删除操作

删除操作的核心是找到目标节点的前一个节点,修改其指针跳过目标节点:

删除指定值的节点

java 复制代码
/**
 * 删除链表中第一个值为val的节点
 * @param head 头节点
 * @param val 要删除的节点值
 * @return 新的头节点(可能改变)
 */
public static ListNode deleteByValue(ListNode head, int val) {
    // 特殊情况1:链表为空
    if (head == null) {
        return null;
    }
    
    // 特殊情况2:头节点就是目标节点
    if (head.val == val) {
        return head.next;  // 头节点后移
    }
    
    ListNode current = head;
    // 找到目标节点的前一个节点
    while (current.next != null && current.next.val != val) {
        current = current.next;
    }
    
    // 如果找到目标节点,修改指针跳过它
    if (current.next != null) {
        current.next = current.next.next;
    }
    
    return head;
}

删除指定索引的节点

java 复制代码
/**
 * 删除指定索引位置的节点
 * @param head 头节点
 * @param index 要删除的位置
 * @return 新的头节点
 */
public static ListNode deleteAtIndex(ListNode head, int index) {
    // 特殊情况:删除头节点(index=0)
    if (index == 0) {
        return head == null ? null : head.next;
    }
    
    ListNode current = head;
    int currentIndex = 0;
    
    // 找到要删除节点的前一个节点
    while (current != null && currentIndex < index - 1) {
        current = current.next;
        currentIndex++;
    }
    
    // 处理索引越界
    if (current == null || current.next == null) {
        throw new IndexOutOfBoundsException("删除索引超出链表长度");
    }
    
    // 跳过目标节点
    current.next = current.next.next;
    return head;
}

三、力扣简单题解题技巧与实战

3.1 核心解题技巧

链表题目的核心难点在于指针操作,掌握以下技巧可大幅提升解题效率:

双指针法:通过两个指针的不同移动速度或位置关系解决问题

  • 快慢指针 :快指针每次移动 2 步,慢指针每次移动 1 步,可用于:
    • 寻找链表的中间节点(快指针到尾时,慢指针在中间)
    • 判断链表是否有环(有环时快慢指针会相遇)
  • 前后指针 :两个指针相距固定距离,可用于:
    • 删除倒数第 n 个节点(前指针先移动 n 步,再同时移动直到前指针到尾)

虚拟头节点(哨兵节点):解决头节点特殊处理的问题

java 复制代码
// 创建虚拟头节点,统一操作逻辑
ListNode dummy = new ListNode(0);
dummy.next = head;  // 指向原头节点

使用虚拟头节点后,头部插入、头部删除等操作可与中间操作统一逻辑,避免单独处理头节点。

画图分析法:复杂操作前先画图,标注指针移动过程。例如反转链表时,通过画图清晰展示每个步骤的指针指向变化,避免逻辑混乱。

边界条件处理:链表题最易出错的地方是边界情况,必须考虑:

  • 链表为空(head == null)
  • 链表只有一个节点(head.next == null)
  • 操作涉及头节点或尾节点

3.2 经典例题实战

例题 1:反转链表(LeetCode 206)

题目:反转一个单链表。

示例:输入 1→2→3→4→5→null,输出 5→4→3→2→1→null。

解法:迭代法(双指针反转)

java 复制代码
public ListNode reverseList(ListNode head) {
    ListNode prev = null;  // 前驱节点
    ListNode curr = head;  // 当前节点
    
    while (curr != null) {
        ListNode nextTemp = curr.next;  // 保存下一个节点
        curr.next = prev;  // 反转指针
        prev = curr;       // 前驱指针后移
        curr = nextTemp;   // 当前指针后移
    }
    
    return prev;  // 反转后prev成为新头节点
}

思路解析:通过三个指针(prev、curr、nextTemp)逐步反转每个节点的指向,时间复杂度 O (n),空间复杂度 O (1)。

例题 2:合并两个有序链表(LeetCode 21)

题目:将两个升序链表合并为一个新的升序链表并返回。

示例:输入 1→2→4 和 1→3→4,输出 1→1→2→3→4→4。

解法:双指针 + 虚拟头节点

java 复制代码
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
    // 创建虚拟头节点,简化操作
    ListNode dummy = new ListNode(0);
    ListNode curr = dummy;  // 当前节点指针
    
    // 双指针遍历两个链表
    while (list1 != null && list2 != null) {
        if (list1.val <= list2.val) {
            curr.next = list1;  // 连接较小节点
            list1 = list1.next; // 移动对应链表指针
        } else {
            curr.next = list2;
            list2 = list2.next;
        }
        curr = curr.next;  // 移动当前指针
    }
    
    // 连接剩余节点(其中一个链表已遍历完)
    curr.next = list1 != null ? list1 : list2;
    
    return dummy.next;  // 返回合并后链表的头节点
}

思路解析:通过虚拟头节点统一处理合并逻辑,双指针比较节点值,每次连接较小的节点,最后连接剩余未遍历的节点。

例题 3:删除链表的倒数第 N 个节点(LeetCode 19)

题目:给你一个链表,删除链表的倒数第 n 个节点,并且返回链表的头节点。

示例:输入 1→2→3→4→5,n=2,输出 1→2→3→5。

解法:双指针(前后指针)

java 复制代码
public ListNode removeNthFromEnd(ListNode head, int n) {
    // 虚拟头节点,处理删除头节点的情况
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    
    ListNode first = dummy;  // 前指针
    ListNode second = dummy; // 后指针
    
    // 前指针先移动n+1步(确保与后指针相距n步)
    for (int i = 0; i <= n; i++) {
        first = first.next;
    }
    
    // 同时移动两个指针,直到前指针到尾
    while (first != null) {
        first = first.next;
        second = second.next;
    }
    
    // 此时second指向倒数第n+1个节点,删除其下一个节点
    second.next = second.next.next;
    
    return dummy.next;
}

思路解析:利用前后指针的固定间距(n 步),当前指针到达尾部时,后指针恰好指向倒数第 n+1 个节点,直接修改指针即可删除目标节点。

四、总结与进阶建议

链表作为基础数据结构,其核心是理解节点间的指针关联和操作逻辑。通过本文的学习,你已掌握:

  • 链表的定义与三种常见类型(重点是单向链表)
  • 链表的创建、遍历、插入、删除等核心操作
  • 双指针、虚拟头节点等解题技巧
  • 力扣简单题的实战思路

想要进一步提升,建议:

  1. 多练习力扣简单链表题(如 LeetCode 206、21、19、83 等),熟悉各类操作场景
  2. 尝试用递归实现链表操作(如递归反转链表),理解递归与迭代的转换
  3. 学习复杂链表问题(如环形链表、相交链表),为中等难度题目打基础

掌握链表不仅能应对算法题,更能培养对指针操作和内存管理的理解,这对后续学习树、图等复杂数据结构至关重要。勤加练习,指针操作会从 "容易出错" 变成 "得心应手"!

相关推荐
我崽不熬夜23 分钟前
掌握Java中的数组与集合:如何灵活处理不同的数据结构?
java·后端·java ee
_码农1213844 分钟前
模拟tomcat接收GET、POST请求
java·tomcat
板板正2 小时前
SpringAI——向量存储(vector store)
java·spring boot·ai
野生技术架构师2 小时前
Spring Boot 定时任务与 xxl-job 灵活切换方案
java·spring boot·后端
苹果醋33 小时前
Java并发编程-Java内存模型(JMM)
java·运维·spring boot·mysql·nginx
你怎么知道我是队长3 小时前
C语言---编译的最小单位---令牌(Token)
java·c语言·前端
小欣加油3 小时前
leetcode 904 水果成篮
c++·算法·leetcode
君万4 小时前
【LeetCode每日一题】56. 合并区间
算法·leetcode·golang
墩墩同学4 小时前
【LeetCode题解】LeetCode 287. 寻找重复数
算法·leetcode·二分查找