鏈表是面試高頻考點,考察指針操作、邊界條件處理、空間換時間思維。本文系統整理 11 道經典題目,提供雙指針、遞歸、棧、快慢指針多種解法,附完整可運行代碼。
class ListNode {
int val;
ListNode next;
ListNode(int val) { this.val = val; }
}
class LinkedListUtil {
// 尾插法構建
public static ListNode build(int... vals) {
ListNode dummy = new ListNode(0), cur = dummy;
for (int v : vals) cur = cur.next = new ListNode(v);
return dummy.next;
}
// 打印
public static void print(ListNode head) {
for (ListNode p = head; p != null; p = p.next)
System.out.print(p.val + (p.next != null ? " -> " : "\n"));
}
// 創建環(用於測試)
public static void createCycle(ListNode head, int pos) {
if (pos < 0) return;
ListNode cycleNode = null, tail = head;
int idx = 0;
while (tail.next != null) {
if (idx == pos) cycleNode = tail;
tail = tail.next;
idx++;
}
if (idx == pos) cycleNode = tail;
tail.next = cycleNode;
}
}
尾插法 O(n),遍歷 O(n)。注意頭節點為空處理。
public int length(ListNode head) {
int len = 0;
for (ListNode p = head; p != null; p = p.next) len++;
return len;
}
public ListNode findKthFromEnd(ListNode head, int k) {
if (head == null || k <= 0) return null;
ListNode fast = head, slow = head;
// fast 先走 k-1 步
for (int i = 0; i < k - 1; i++) {
if (fast.next == null) return null; // k > 長度
fast = fast.next;
}
// 同步前進,fast 到達尾部時 slow 指向倒數第 k
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
public ListNode findMiddle(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow; // 偶數長度返回第 n/2+1 個
}
// 迭代法 O(1) 空間
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0), cur = dummy;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) { cur.next = l1; l1 = l1.next; }
else { cur.next = l2; l2 = l2.next; }
cur = cur.next;
}
cur.next = (l1 != null) ? l1 : l2;
return dummy.next;
}
// 遞歸法 簡潔但 O(n) 棧空間
public ListNode mergeRecursive(ListNode l1, ListNode l2) {
if (l1 == null) return l2;
if (l2 == null) return l1;
if (l1.val < l2.val) { l1.next = mergeRecursive(l1.next, l2); return l1; }
else { l2.next = mergeRecursive(l1, l2.next); return l2; }
}
// 迭代法:pre 保存前驅,cur 當前,next 保存後繼
public ListNode reverseIterative(ListNode head) {
ListNode pre = null, cur = head;
while (cur != null) {
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
// 遞歸法:假設 head.next 已反轉,調整指向
public ListNode reverseRecursive(ListNode head) {
if (head == null || head.next == null) return head;
ListNode newHead = reverseRecursive(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
// 顯式棧 可控、無棧溢出風險
public void reversePrintStack(ListNode head) {
Deque<Integer> stack = new ArrayDeque<>();
for (ListNode p = head; p != null; p = p.next) stack.push(p.val);
while (!stack.isEmpty()) System.out.println(stack.pop());
}
// 遞歸 代碼極簡但深度受限
public void reversePrintRecursive(ListNode head) {
if (head == null) return;
reversePrintRecursive(head.next);
System.out.println(head.val);
}
public boolean hasCycle(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) return true;
}
return false;
}
public int cycleLength(ListNode head) {
ListNode meet = findMeetNode(head);
if (meet == null) return 0;
int len = 1;
for (ListNode p = meet.next; p != meet; p = p.next) len++;
return len;
}
private ListNode findMeetNode(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) return slow;
}
return null;
}
原理:相遇時,slow 走 a+b,fast 走 a+b+c+b = 2(a+b) → a = c。
即:從頭節點與相遇點同步前進,相遇處即入口。
public ListNode detectCycleEntrance(ListNode head) {
ListNode meet = findMeetNode(head);
if (meet == null) return null;
ListNode p1 = head, p2 = meet;
while (p1 != p2) { p1 = p1.next; p2 = p2.next; }
return p1;
}
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
int lenA = length(headA), lenB = length(headB);
ListNode longList = lenA > lenB ? headA : headB;
ListNode shortList = lenA > lenB ? headB : headA;
int diff = Math.abs(lenA - lenB);
while (diff-- > 0) longList = longList.next;
while (longList != shortList) {
longList = longList.next;
shortList = shortList.next;
}
return longList; // null 表示無交點
}
| 題目 | 時間 | 空間 | 核心技巧 |
|---|---|---|---|
| 倒數第 K | O(n) | O(1) | 雙指針間距 k |
| 中間節點 | O(n) | O(1) | 快慢指針 2:1 |
| 合併有序 | O(m+n) | O(1)/O(m+n) | 歸併排序 merge 步 |
| 反轉 | O(n) | O(1)/O(n) | 三指針/遞歸回溯 |
| 逆序打印 | O(n) | O(n) | 棧/系統棧 |
| 判環 | O(n) | O(1) | Floyd 快慢指針 |
| 環長度 | O(n) | O(1) | 相遇點繞環計數 |
| 環入口 | O(n) | O(1) | a=c 數學性質 |
| 相交節點 | O(m+n) | O(1) | 長度差對齊 |
鏈表題目核心考察指針操作熟練度與空間換時間思維。掌握雙指針(快慢/間距)、虛擬頭節點、遞歸回溯三大核心模板,配合嚴謹的邊界測試,可應對 90% 以上面試題目。建議在 LeetCode 刷對應題目(206, 21, 141, 142, 160, 19, 876)鞏固肌肉記憶。