单链表面试题精讲:从基础操作到高级技巧
1. 单链表基础回顾与面试题概览单链表作为数据结构中最基础的链式存储结构在技术面试中出现的频率居高不下。我见过太多候选人因为对单链表的基本操作理解不够深入在面试中错失良机。让我们先快速回顾单链表的核心特性每个节点包含数据域和指针域指针域存储下一个节点的地址。与数组不同单链表的节点在内存中不必连续存储通过指针串联形成逻辑上的线性结构。这种特性带来了插入/删除O(1)时间复杂度的优势但牺牲了随机访问能力必须从头遍历。面试中常见的单链表题目主要考察以下几个维度基础操作能力遍历、插入、删除边界条件处理空链表、头尾节点算法思维双指针、递归空间复杂度优化原地操作接下来我将拆解5类高频面试题包含代码实现、复杂度分析和易错点。这些题目来自我过去三年作为面试官的真实题库以及LeetCode等平台的热门题目。2. 单链表基本操作面试题精讲2.1 链表长度计算与遍历陷阱计算链表长度看似简单但隐藏着几个关键细节public int getLength(ListNode head) { if (head null) return 0; // 空链表判断 int count 0; ListNode current head; while (current ! null) { // 注意不是current.next count; current current.next; } return count; }常见错误包括忽略头节点为null的情况循环条件误用current.next导致少计数一次修改了原链表头节点应用临时变量current时间复杂度O(n)空间复杂度O(1)。这是大多数链表题的基础操作建议熟练掌握。2.2 倒数第K个节点查找的双指针技巧这是经典的快慢指针应用场景public ListNode findKthFromEnd(ListNode head, int k) { if (head null || k 0) return null; ListNode fast head, slow head; // 快指针先走k步 for (int i 0; i k; i) { if (fast null) return null; // k超过链表长度 fast fast.next; } // 双指针同步前进 while (fast ! null) { fast fast.next; slow slow.next; } return slow; }这个解法只需一次遍历时间复杂度O(n)。关键点在于处理k大于链表长度的边界情况快指针移动k步后慢指针才开始移动当快指针到达末尾时慢指针正好在倒数第k个位置3. 链表反转的三种实现方式3.1 迭代法反转链表最经典的解法需要维护三个指针public ListNode reverseList(ListNode head) { ListNode prev null; ListNode curr head; while (curr ! null) { ListNode nextTemp curr.next; // 保存下一个节点 curr.next prev; // 反转指针 prev curr; // 前移prev curr nextTemp; // 前移curr } return prev; // 新头节点 }这个实现的空间复杂度是O(1)因为只使用了固定数量的额外空间。常见错误包括丢失节点引用必须先保存curr.next反转后未正确返回新头节点应该是prev不是curr3.2 递归法实现反转递归解法更简洁但更难理解public ListNode reverseListRecursive(ListNode head) { if (head null || head.next null) { return head; } ListNode p reverseListRecursive(head.next); head.next.next head; // 反转指针 head.next null; // 断开原指针 return p; }递归深度为n空间复杂度O(n)。关键点在于基准条件处理空链表或单节点链表递归反转后续链表将当前节点连接到已反转链表的末尾3.3 头插法反转链表利用虚拟头节点实现public ListNode reverseWithDummy(ListNode head) { ListNode dummy new ListNode(-1); ListNode curr head; while (curr ! null) { ListNode next curr.next; curr.next dummy.next; // 将当前节点插入dummy之后 dummy.next curr; curr next; } return dummy.next; }这种方法在需要保持原链表不被破坏的场景特别有用因为可以随时通过dummy节点访问新链表。4. 链表排序与合并问题4.1 合并两个有序链表经典的归并思路public ListNode mergeTwoLists(ListNode l1, ListNode l2) { ListNode dummy new ListNode(-1); ListNode curr dummy; while (l1 ! null l2 ! null) { if (l1.val l2.val) { curr.next l1; l1 l1.next; } else { curr.next l2; l2 l2.next; } curr curr.next; } // 连接剩余部分 curr.next (l1 ! null) ? l1 : l2; return dummy.next; }时间复杂度O(mn)空间复杂度O(1)。注意点使用dummy节点简化头节点处理最后要处理未遍历完的链表剩余部分保持稳定性相等时优先选择l1的节点4.2 链表排序的归并实现结合归并排序和链表合并public ListNode sortList(ListNode head) { if (head null || head.next null) return head; // 使用快慢指针找到中点 ListNode slow head, fast head, prev null; while (fast ! null fast.next ! null) { prev slow; slow slow.next; fast fast.next.next; } prev.next null; // 切断链表 // 递归排序两个子链表 ListNode l1 sortList(head); ListNode l2 sortList(slow); // 合并已排序链表 return mergeTwoLists(l1, l2); }时间复杂度O(nlogn)空间复杂度O(logn)递归栈。这是链表排序的最优解法比插入排序更适合长链表。5. 环形链表检测与入口定位5.1 快慢指针检测环形链表public boolean hasCycle(ListNode head) { if (head null) return false; 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; }时间复杂度O(n)空间复杂度O(1)。关键点快指针每次移动两步慢指针每次一步相遇说明有环快指针到达null说明无环初始条件处理空链表情况5.2 环形链表入口定位找到相遇点后数学推导可得public ListNode detectCycle(ListNode head) { ListNode meet getMeetNode(head); if (meet null) return null; ListNode p1 head, p2 meet; while (p1 ! p2) { p1 p1.next; p2 p2.next; } return p1; } private ListNode getMeetNode(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; }这个算法基于一个重要的数学关系从head到环入口的距离等于从相遇点到环入口的距离。因此在找到相遇点后用两个指针分别从head和相遇点出发相遇点即为环入口。6. 复杂链表操作与边界处理6.1 删除倒数第N个节点结合虚拟头节点和双指针public ListNode removeNthFromEnd(ListNode head, int n) { ListNode dummy new ListNode(0); dummy.next head; ListNode fast dummy, slow dummy; // 快指针先走n1步 for (int i 0; i n; i) { if (fast null) return head; // n超出长度 fast fast.next; } // 同步移动直到快指针到达末尾 while (fast ! null) { fast fast.next; slow slow.next; } // 删除slow的下一个节点 slow.next slow.next.next; return dummy.next; }使用虚拟头节点可以统一处理删除头节点的情况。时间复杂度O(n)空间复杂度O(1)。6.2 链表相交问题判断两个链表是否相交并找到交点public ListNode getIntersectionNode(ListNode headA, ListNode headB) { if (headA null || headB null) return null; ListNode pA headA, pB headB; while (pA ! pB) { pA (pA null) ? headB : pA.next; pB (pB null) ? headA : pB.next; } return pA; }这个巧妙的解法让两个指针分别遍历两个链表最终会在交点相遇或同时到达null。时间复杂度O(mn)空间复杂度O(1)。7. 链表操作优化技巧总结经过这些题目的训练我总结出链表操作的几个核心技巧虚拟头节点处理头节点可能被修改的情况避免复杂的边界判断快慢指针解决环检测、中点查找、倒数第k个等问题多指针协同反转链表等操作需要维护多个指针引用递归思维某些问题如反转、合并用递归实现更简洁画图辅助复杂操作前先画出节点和指针变化示意图在面试中建议先明确问题要求与面试官确认边界条件如链表是否可能为空、能否修改原链表等然后选择合适的方法实现。写完代码后务必用测试用例验证空链表、单节点链表、头尾节点等特殊情况。