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

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 个节点,直接修改指针即可删除目标节点。

四、总结与进阶建议

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

链表的定义与三种常见类型(重点是单向链表)

链表的创建、遍历、插入、删除等核心操作

双指针、虚拟头节点等解题技巧

力扣简单题的实战思路

想要进一步提升,建议:

多练习力扣简单链表题(如 LeetCode 206、21、19、83 等),熟悉各类操作场景

尝试用递归实现链表操作(如递归反转链表),理解递归与迭代的转换

学习复杂链表问题(如环形链表、相交链表),为中等难度题目打基础

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

更多创意作品