1. 链表算法实战:从LeetCode高频题掌握核心技巧
链表作为数据结构中的基础类型,在算法面试中出现的频率极高。最近在准备技术面试时,我系统刷了一遍LeetCode上的链表专题,发现其中160相交链表、206反转链表等题目堪称经典中的经典。这些题目看似简单,但要在面试中快速写出无bug的解法并不容易。今天我就结合自己的刷题经验,分享这些链表题目的解题思路和避坑技巧。
链表操作的核心在于指针(引用)的灵活运用。与数组不同,链表节点在内存中不是连续存储的,这带来了O(1)时间插入删除的优势,但也导致无法随机访问。理解这一点是解决所有链表问题的基础。下面我将分类解析这些高频题目,重点说明每种类型的解题框架和易错点。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础操作类题目解析
2.1 206反转链表 - 指针操作的入门课
反转链表是链表操作中最基础的题目,但也是面试官最爱考察的题目之一。先看题目要求:给定单链表的头节点head,请反转链表并返回反转后的头节点。
迭代解法是最直观的方法:
python复制def reverseList(head):
prev = None
curr = head
while curr:
next_node = curr.next # 临时保存下一个节点
curr.next = prev # 反转指针方向
prev = curr # prev指针前移
curr = next_node # curr指针前移
return prev
这个解法的时间复杂度是O(n),空间复杂度是O(1)。关键点在于:
- 需要三个指针:prev记录前驱节点,curr当前节点,next_node临时保存下一个节点
- 每次迭代只处理curr节点的next指针指向
- 最后返回的是prev指针,因为它会停在新的头节点位置
常见错误:忘记保存next_node就直接修改curr.next,这会导致链表断裂。我在第一次写时就犯了这个错误。
递归解法更简洁但更难理解:
python复制def reverseList(head):
if not head or not head.next:
return head
new_head = reverseList(head.next)
head.next.next = head
head.next = None
return new_head
递归的核心思想是把链表分为头节点和剩余部分,先反转剩余部分,再处理头节点。这个解法空间复杂度是O(n)因为递归栈的开销。
2.2 24两两交换链表节点 - 指针操作的进阶
题目要求:给定链表,两两交换相邻节点,返回交换后的链表。如1->2->3->4变为2->1->4->3。
这道题的关键是处理好指针的重新指向,同时不丢失对后续节点的引用。我的解法是使用dummy节点简化边界条件:
python复制def swapPairs(head):
dummy = ListNode(0)
dummy.next = head
prev = dummy
while prev.next and prev.next.next:
first = prev.next
second = first.next
# 执行交换
prev.next = second
first.next = second.next
second.next = first
# 移动prev指针
prev = first
return dummy.next
这个解法的时间复杂度是O(n),空间复杂度是O(1)。几个关键点:
- dummy节点的使用避免了处理头节点交换的特殊情况
- 需要三个指针参与交换:prev记录前驱,first和second是要交换的两个节点
- 交换后prev指针要正确移动到下一组的前驱位置
经验:链表问题中,dummy节点能极大简化边界条件的处理。我在做这道题时,最初没有用dummy节点,结果处理头节点交换时各种判断,代码非常混乱。
3. 环形链表问题解析
3.1 141环形链表 - 快慢指针的经典应用
题目要求:判断链表中是否有环。这是快慢指针算法最典型的应用场景。
解法如下:
python复制def hasCycle(head):
if not head or not head.next:
return False
slow = head
fast = head.next
while slow != fast:
if not fast or not fast.next:
return False
slow = slow.next
fast = fast.next.next
return True
这个算法的时间复杂度是O(n),空间复杂度是O(1)。原理是:
- 慢指针每次走一步,快指针每次走两步
- 如果有环,快指针最终会追上慢指针
- 如果快指针到达链表尾部(遇到None),说明无环
注意事项:循环条件应该是slow != fast而不是检查快指针是否为None,这样可以减少一次不必要的判断。我最初写错了这个条件,导致边界情况处理不当。
3.2 142环形链表II - 找出环的入口
这是141题的进阶版,要求找出环的入口节点。这需要数学推导:
- 设链表头到环入口距离为a,环入口到相遇点距离为b,相遇点到环入口距离为c
- 相遇时,慢指针走了a+b,快指针走了a+b+n(b+c)
- 因为快指针速度是慢指针两倍:2(a+b)=a+b+n(b+c) => a=(n-1)(b+c)+c
- 这意味着从相遇点和链表头同时出发,最终会在环入口相遇
代码实现:
python复制def detectCycle(head):
slow = fast = head
has_cycle = False
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
has_cycle = True
break
if not has_cycle:
return None
ptr = head
while ptr != slow:
ptr = ptr.next
slow = slow.next
return ptr
这个算法同样保持O(n)时间复杂度和O(1)空间复杂度。关键在于第二阶段移动ptr和slow指针,它们最终会在环入口相遇。
4. 链表与其他数据结构的结合
4.1 234回文链表 - 结合栈和快慢指针
题目要求:判断链表是否为回文结构。挑战是要求O(n)时间和O(1)空间。
我的解法分三步:
- 使用快慢指针找到链表中点
- 反转后半部分链表
- 比较前后两部分是否相同
python复制def isPalindrome(head):
if not head or not head.next:
return True
# 找中点
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# 反转后半部分
prev = None
curr = slow
while curr:
next_node = curr.next
curr.next = prev
prev = curr
curr = next_node
# 比较前后部分
first = head
second = prev
while second:
if first.val != second.val:
return False
first = first.next
second = second.next
return True
这个解法满足题目要求,时间复杂度O(n),空间复杂度O(1)。需要注意:
- 找中点时,奇数长度和偶数长度处理略有不同
- 反转后半部分后不需要恢复,因为题目只要求判断不回写原链表
- 比较时只需要以后半部分长度为标准
踩坑记录:我最初在反转后半部分后,比较时没有正确处理奇数长度情况,导致中间节点被重复比较。后来通过限制以后半部分长度为标准解决了这个问题。
4.2 148排序链表 - 归并排序的应用
题目要求:在O(nlogn)时间复杂度和常数空间复杂度下对链表排序。
最适合链表的O(nlogn)排序算法是归并排序,因为链表可以O(1)空间完成合并操作。实现分为三步:
- 使用快慢指针找到中点分割链表
- 递归排序两个子链表
- 合并两个有序链表
python复制def sortList(head):
if not head or not head.next:
return head
# 分割链表
slow = fast = head
prev = None
while fast and fast.next:
prev = slow
slow = slow.next
fast = fast.next.next
prev.next = None
# 递归排序
left = sortList(head)
right = sortList(slow)
# 合并
return merge(left, right)
def merge(l1, l2):
dummy = ListNode(0)
curr = dummy
while l1 and l2:
if l1.val < l2.val:
curr.next = l1
l1 = l1.next
else:
curr.next = l2
l2 = l2.next
curr = curr.next
curr.next = l1 if l1 else l2
return dummy.next
这个实现的时间复杂度是O(nlogn),空间复杂度由于递归是O(logn)。要优化到O(1)空间,可以改用迭代方式实现归并排序。
5. 复杂链表操作问题
5.1 138随机链表复制 - 哈希表或原地复制
题目要求:复制一个含有随机指针的链表。每个节点除了next指针,还有一个random指针可能指向任意节点。
解法一:使用哈希表存储原节点和复制节点的映射关系
python复制def copyRandomList(head):
if not head:
return None
mapping = {}
curr = head
# 第一次遍历创建所有节点
while curr:
mapping[curr] = Node(curr.val)
curr = curr.next
# 第二次遍历设置指针
curr = head
while curr:
if curr.next:
mapping[curr].next = mapping[curr.next]
if curr.random:
mapping[curr].random = mapping[curr.random]
curr = curr.next
return mapping[head]
这个解法时间空间复杂度都是O(n)。还有一种O(1)空间的解法,通过在原链表中插入复制节点:
- 为每个原节点创建一个复制节点,插入到原节点后面
- 设置复制节点的random指针
- 分离两个链表
python复制def copyRandomList(head):
if not head:
return None
# 插入复制节点
curr = head
while curr:
new_node = Node(curr.val)
new_node.next = curr.next
curr.next = new_node
curr = new_node.next
# 设置random指针
curr = head
while curr:
if curr.random:
curr.next.random = curr.random.next
curr = curr.next.next
# 分离链表
curr = head
new_head = head.next
while curr:
temp = curr.next
curr.next = temp.next
if temp.next:
temp.next = temp.next.next
curr = curr.next
return new_head
这个解法将空间复杂度优化到O(1),但代码相对复杂。面试时可以先实现哈希表版本,再优化到原地复制。
5.2 160相交链表 - 双指针的巧妙运用
题目要求:找出两个单链表相交的起始节点。注意链表必须保持原始结构。
我的解法利用了双指针遍历两个链表的技巧:
python复制def getIntersectionNode(headA, headB):
if not headA or not headB:
return None
ptrA = headA
ptrB = headB
while ptrA != ptrB:
ptrA = ptrA.next if ptrA else headB
ptrB = ptrB.next if ptrB else headA
return ptrA
这个算法的时间复杂度是O(m+n),空间复杂度是O(1)。原理是:
- 两个指针分别遍历两个链表
- 当一个指针到达链表末尾时,转向另一个链表头部
- 如果链表相交,两个指针会在交点相遇;否则会同时到达None
关键点:这个算法巧妙地处理了链表长度不等的情况,通过让两个指针都遍历两个链表,确保它们走过的总长度相同。我第一次看到这个解法时觉得非常精妙。
6. 链表与其他算法的结合
6.1 2两数相加 - 模拟数学加法
题目要求:给定两个非空链表表示两个非负整数,每位数字按逆序存储,返回两数之和的链表表示。
解法是模拟数学加法过程:
python复制def addTwoNumbers(l1, l2):
dummy = ListNode(0)
curr = dummy
carry = 0
while l1 or l2 or carry:
sum_val = carry
if l1:
sum_val += l1.val
l1 = l1.next
if l2:
sum_val += l2.val
l2 = l2.next
carry = sum_val // 10
curr.next = ListNode(sum_val % 10)
curr = curr.next
return dummy.next
这个算法的时间复杂度是O(max(m,n)),空间复杂度是O(max(m,n))。需要注意:
- 使用dummy节点简化头节点处理
- 循环条件要包括carry,因为最高位可能有进位
- 每次计算要更新进位值carry
易错点:我最初忘记在循环条件中包含carry,导致最高位有进位时结果错误。这个bug在测试用例[5]和[5]时暴露出来。
6.2 19删除倒数第N个节点 - 快慢指针的变种
题目要求:删除链表的倒数第N个节点。
解法是使用快慢指针,让快指针先走N步:
python复制def removeNthFromEnd(head, n):
dummy = ListNode(0)
dummy.next = head
fast = slow = dummy
# 快指针先走n步
for _ in range(n):
fast = fast.next
# 同时移动直到快指针到达末尾
while fast.next:
slow = slow.next
fast = fast.next
# 删除节点
slow.next = slow.next.next
return dummy.next
这个算法的时间复杂度是O(L),空间复杂度是O(1),其中L是链表长度。关键点:
- dummy节点的使用避免处理删除头节点的特殊情况
- 快指针先走n步,然后快慢指针一起移动
- 当快指针到达末尾时,慢指针指向要删除节点的前驱
注意事项:要测试n等于链表长度的情况(即删除头节点)。我最初没有用dummy节点,导致这个边界条件处理出错。
链表问题的核心在于指针操作的精准控制。经过这些题目的训练,我对链表的理解更加深入了。在实际面试中,建议先明确思路再写代码,特别注意边界条件的处理。对于复杂问题,可以先和面试官讨论解法,再逐步优化。
