1. 链表基础概念与Java实现
链表(Linked List)是一种物理存储单元上非连续、非顺序的线性数据结构,由一系列节点(Node)组成。每个节点包含两个部分:数据域(data)和指针域(next)。与数组不同,链表在内存中不需要连续的空间,而是通过指针将零散的内存块串联起来使用。
Java中链表的核心实现类是LinkedList,它位于java.util包中,实现了List和Deque接口。LinkedList底层采用双向链表实现,这意味着每个节点不仅保存了下一个节点的引用,还保存了前一个节点的引用。
java复制// 典型的链表节点定义
class Node<E> {
E data;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.data = element;
this.next = next;
this.prev = prev;
}
}
在实际开发中,链表特别适合频繁插入和删除操作的场景。因为链表不需要像数组那样在插入或删除时移动大量元素,只需要修改相邻节点的指针即可。但这也带来了随机访问效率低下的问题,因为链表必须从头节点开始逐个遍历。
提示:当需要频繁随机访问元素时,ArrayList是更好的选择;当需要频繁在列表中间插入/删除元素时,LinkedList性能更优。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Java链表的核心操作实现
2.1 链表的基本操作
Java中LinkedList类提供了丰富的操作方法,以下是最常用的几种:
- 添加元素:
java复制LinkedList<String> list = new LinkedList<>();
list.add("A"); // 尾部添加
list.addFirst("B"); // 头部添加
list.addLast("C"); // 尾部添加(与add相同)
list.add(1, "D"); // 在指定位置插入
- 删除元素:
java复制list.remove(); // 删除头部元素
list.removeFirst(); // 删除头部元素
list.removeLast(); // 删除尾部元素
list.remove("A"); // 删除指定元素
list.remove(1); // 删除指定位置元素
- 访问元素:
java复制String first = list.getFirst(); // 获取头部元素
String last = list.getLast(); // 获取尾部元素
String element = list.get(2); // 获取指定位置元素
- 其他常用操作:
java复制int size = list.size(); // 获取链表长度
boolean isEmpty = list.isEmpty(); // 判断是否为空
boolean contains = list.contains("A"); // 判断是否包含某元素
int index = list.indexOf("B"); // 获取元素首次出现的位置
2.2 链表的遍历方式
Java中遍历LinkedList有多种方式,各有优缺点:
- 普通for循环(性能较差):
java复制for(int i=0; i<list.size(); i++) {
System.out.println(list.get(i));
}
- 增强for循环(推荐):
java复制for(String item : list) {
System.out.println(item);
}
- 使用迭代器(适合需要删除元素的情况):
java复制Iterator<String> iterator = list.iterator();
while(iterator.hasNext()) {
String item = iterator.next();
System.out.println(item);
// iterator.remove(); 可以在遍历时安全删除元素
}
- 使用ListIterator(可以双向遍历):
java复制ListIterator<String> listIterator = list.listIterator();
while(listIterator.hasNext()) {
System.out.println(listIterator.next());
}
while(listIterator.hasPrevious()) {
System.out.println(listIterator.previous());
}
注意:LinkedList的get(int index)方法性能较差,因为它需要从头或尾开始遍历到指定位置。对于频繁随机访问的场景,应考虑使用ArrayList。
3. 链表的高级应用与性能优化
3.1 链表与递归算法
链表的结构天然适合递归操作,许多链表问题都可以用递归优雅地解决。例如反转链表:
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;
}
另一个经典例子是合并两个有序链表:
java复制public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1 == null) return l2;
if(l2 == null) return l1;
if(l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
3.2 快慢指针技巧
快慢指针是解决链表问题的强大工具,常用于:
- 检测链表是否有环:
java复制public boolean hasCycle(ListNode head) {
if(head == null) return false;
ListNode slow = head;
ListNode fast = head.next;
while(fast != null && fast.next != null) {
if(slow == fast) return true;
slow = slow.next;
fast = fast.next.next;
}
return false;
}
- 寻找链表的中间节点:
java复制public ListNode middleNode(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while(fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
- 寻找链表的倒数第k个节点:
java复制public ListNode getKthFromEnd(ListNode head, int k) {
ListNode fast = head;
ListNode slow = head;
for(int i=0; i<k; i++) {
fast = fast.next;
}
while(fast != null) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
3.3 虚拟头节点技巧
在处理链表问题时,引入虚拟头节点(dummy node)可以简化很多边界条件的处理:
java复制public ListNode removeElements(ListNode head, int val) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode curr = dummy;
while(curr.next != null) {
if(curr.next.val == val) {
curr.next = curr.next.next;
} else {
curr = curr.next;
}
}
return dummy.next;
}
虚拟头节点的优势在于:
- 统一处理头节点和其他节点的逻辑
- 避免空指针异常
- 简化删除操作,不需要特殊处理头节点
4. 链表常见问题与解决方案
4.1 链表常见面试题解析
- 反转链表(迭代法):
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;
}
- 两数相加(链表表示的数字相加):
java复制public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode curr = dummy;
int carry = 0;
while(l1 != null || l2 != null || carry != 0) {
int sum = carry;
if(l1 != null) {
sum += l1.val;
l1 = l1.next;
}
if(l2 != null) {
sum += l2.val;
l2 = l2.next;
}
carry = sum / 10;
curr.next = new ListNode(sum % 10);
curr = curr.next;
}
return dummy.next;
}
- 合并K个有序链表(优先队列解法):
java复制public ListNode mergeKLists(ListNode[] lists) {
if(lists == null || lists.length == 0) return null;
PriorityQueue<ListNode> queue = new PriorityQueue<>((a,b)->a.val-b.val);
for(ListNode node : lists) {
if(node != null) {
queue.offer(node);
}
}
ListNode dummy = new ListNode(0);
ListNode curr = dummy;
while(!queue.isEmpty()) {
curr.next = queue.poll();
curr = curr.next;
if(curr.next != null) {
queue.offer(curr.next);
}
}
return dummy.next;
}
4.2 链表操作中的常见错误
- 空指针异常:
- 访问null节点的next或val属性
- 解决方案:在访问节点属性前总是检查是否为null
- 循环引用:
- 错误地修改指针导致链表出现环
- 解决方案:在修改指针前保存必要的临时变量
- 边界条件处理不当:
- 空链表
- 单节点链表
- 头节点/尾节点的特殊处理
- 解决方案:使用虚拟头节点简化逻辑
- 性能问题:
- 在LinkedList上频繁使用get(index)方法
- 解决方案:使用迭代器或增强for循环遍历
4.3 链表与多线程
LinkedList不是线程安全的类,在多线程环境下使用时需要特别注意:
- 同步方案:
java复制List<String> syncList = Collections.synchronizedList(new LinkedList<>());
- 使用CopyOnWriteArrayList(适合读多写少的场景):
java复制CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
- 手动加锁:
java复制LinkedList<String> list = new LinkedList<>();
// 写操作
synchronized(list) {
list.add("item");
}
// 读操作
synchronized(list) {
String item = list.get(0);
}
重要提示:在高并发场景下,考虑使用ConcurrentLinkedQueue等专门的并发集合类,它们为并发访问做了优化。
