1. 链表基础与问题概述
链表作为数据结构中的经典存在,其重要性不亚于数组。在实际工程和算法面试中,链表操作占据了相当大的比重。今天我们要解决的四个问题——两两交换节点、删除倒数第N个节点、链表相交判断和环形链表检测,涵盖了链表操作中最具代表性的几类问题。
链表与数组最大的区别在于其非连续的内存分布。每个节点包含数据域和指针域,通过指针将零散的内存块串联起来。这种结构使得链表在插入和删除操作上具有O(1)的时间复杂度优势,但随机访问的效率较低,为O(n)。
在解决链表问题时,有几个核心技巧需要掌握:
- 虚拟头节点(dummy node)的使用可以简化头节点的特殊处理
- 快慢指针法是解决环形链表和中间节点问题的利器
- 指针的保存与更新顺序直接影响代码的正确性
- 递归思想在某些链表问题中能提供更简洁的解决方案
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 24. 两两交换链表中的节点
2.1 问题描述与示例
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。你不能只是单纯改变节点内部的值,而是需要实际进行节点交换。
示例:
输入:1->2->3->4
输出:2->1->4->3
2.2 迭代解法详解
迭代法是解决这个问题最直观的方法。我们需要维护几个关键指针:
- pre:指向待交换两个节点的前一个节点
- first:待交换的第一个节点
- second:待交换的第二个节点
操作步骤:
- 创建虚拟头节点dummy,其next指向head
- 初始化pre指向dummy
- 当pre.next和pre.next.next都不为空时循环:
a. first = pre.next
b. second = pre.next.next
c. 执行交换:
pre.next = second
first.next = second.next
second.next = first
d. pre移动到first(交换后的第二个节点) - 返回dummy.next
关键点:交换前必须保存first节点,因为first.next会被修改。指针更新的顺序也很重要,错误的顺序会导致链表断裂。
2.3 递归解法分析
递归的思路更加简洁:
- 基线条件:当前节点或下一节点为空,直接返回
- 递归单元:
a. 保存第二个节点
b. 第一个节点指向递归结果(后续已处理的链表)
c. 第二个节点指向第一个节点 - 返回新的头节点(原第二个节点)
递归虽然代码简洁,但空间复杂度为O(n)(递归栈空间),而迭代法是O(1)。
3. 19. 删除链表的倒数第N个节点
3.1 问题描述
给定一个链表,删除链表的倒数第n个节点,并且返回链表的头节点。
示例:
输入:1->2->3->4->5, n=2
输出:1->2->3->5
3.2 双指针经典解法
这是快慢指针的经典应用场景:
- 创建dummy节点指向head
- 初始化fast和slow都指向dummy
- fast先走n步
- 然后fast和slow一起走,直到fast到达末尾
- 此时slow.next就是要删除的节点
- 执行删除:slow.next = slow.next.next
- 返回dummy.next
注意事项:使用dummy节点可以统一处理删除头节点的情况。n的有效性检查也很重要,确保不会超出链表长度。
3.3 边界条件处理
几个容易出错的边界情况:
- 链表为空
- n等于链表长度(删除头节点)
- n大于链表长度
- n为0或负数
在实际编码中,应该先处理这些特殊情况,或者确保算法能正确处理它们。
4. 面试题02.07. 链表相交
4.1 问题理解
给定两个单链表的头节点headA和headB,找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回null。
注意:
- 相交是基于引用而非值
- 链表必须保持原始结构
- 时间复杂度O(m+n),空间复杂度O(1)
4.2 双指针解法
精妙的双指针解法步骤:
- 初始化指针pA和pB分别指向headA和headB
- 同时移动两个指针,每次前进一步
- 当pA到达末尾时,重定向到headB
- 当pB到达末尾时,重定向到headA
- 当pA和pB相遇时,即为交点(或都为null)
这个方法的原理是:两个指针走过的总长度都是A+B,如果有交点,必会在交点相遇;否则会同时到达null。
4.3 长度对齐法
另一种思路是先计算两个链表的长度差:
- 遍历得到两个链表的长度lenA和lenB
- 长的链表先走|lenA-lenB|步
- 然后两个指针同步前进,比较是否相同
这种方法虽然也能解决问题,但代码相对复杂,且需要两次遍历计算长度。
5. 142. 环形链表II
5.1 问题描述
给定一个链表,返回链表开始入环的第一个节点。如果链表无环,则返回null。
5.2 快慢指针数学证明
Floyd判圈算法的步骤:
- 初始化slow和fast都指向head
- slow每次走一步,fast每次走两步
- 如果fast遇到null,说明无环
- 否则两者会在环内某点相遇
- 此时将一个指针移回head,两者同步前进,再次相遇点即为环入口
数学原理:
设头节点到环入口距离为a,环入口到相遇点距离为b,相遇点到环入口距离为c。
当相遇时:
slow走了a+b
fast走了a+n(b+c)+b
因为fast是slow两倍速度:2(a+b)=a+(n+1)b+nc
化简得:a=(n-1)(b+c)+c
这意味着从head和相遇点同时出发,必在环入口相遇。
5.3 哈希表解法
虽然空间复杂度较高(O(n)),但思路简单:
- 遍历链表,用集合记录访问过的节点
- 第一个重复访问的节点就是环入口
- 如果遍历结束无重复,则无环
这种方法在工程中可能更易理解和维护,但面试时通常期望更优的空间解法。
6. 链表问题通用技巧
6.1 调试与可视化
链表问题调试困难,建议:
- 打印链表时加入标记,如:1->2->3->(回到1)形成环
- 限制最大打印长度防止无限循环
- 画图辅助理解指针变化
6.2 常见错误
- 指针操作顺序错误导致链表断裂
- 未正确处理头节点或尾节点
- 循环条件不完整导致空指针异常
- 在环形链表中忘记设置终止条件
6.3 复杂度分析
大多数链表问题:
- 时间复杂度:O(n),通常只需一次或几次遍历
- 空间复杂度:O(1)的解法通常更受青睐
在实际工程中,可能需要权衡代码可读性和性能,有时使用额外空间换取更清晰的逻辑也是合理的选择。
