在数据结构的学习中,链表与数组是两种最基础的线性结构,却有着截然不同的特性。相比于数组的连续内存存储,链表通过指针串联节点的灵活设计,在插入、删除操作中展现出独特优势。本文将系统梳理 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 等),熟悉各类操作场景
- 尝试用递归实现链表操作(如递归反转链表),理解递归与迭代的转换
- 学习复杂链表问题(如环形链表、相交链表),为中等难度题目打基础
掌握链表不仅能应对算法题,更能培养对指针操作和内存管理的理解,这对后续学习树、图等复杂数据结构至关重要。勤加练习,指针操作会从 "容易出错" 变成 "得心应手"!