1. 链表基础与常见操作解析
链表作为数据结构中的基础类型,在算法面试中出现的频率极高。与数组不同,链表通过指针将零散的内存块串联起来,每个节点包含数据和指向下一个节点的指针。这种非连续存储特性使得链表在插入、删除操作上具有O(1)时间复杂度优势,但随机访问效率较低(O(n))。
1.1 链表操作的三大核心技巧
**虚拟头节点(Dummy Node)**是处理链表问题的利器。当需要对头节点进行修改时,引入一个不存储实际数据的节点作为临时头节点,可以避免复杂的边界条件判断。例如在删除操作中:
python复制dummy = ListNode(0)
dummy.next = head
# 后续操作都基于dummy进行
return dummy.next # 返回真正的头节点
双指针法在链表问题中应用广泛,主要包括:
- 快慢指针:用于检测环、找中点等(如快指针每次走两步,慢指针一步)
- 前后指针:用于删除倒数第N个节点等场景
- 分离指针:处理两个链表相交问题时常用
指针的交换与反转是链表操作的核心技能。以反转链表为例,需要维护三个指针:prev、current、next,通过逐步调整指向实现反转:
python复制prev = None
current = head
while current:
next_node = current.next # 临时保存下一个节点
current.next = prev # 反转指向
prev = current # 移动prev
current = next_node # 移动current
return prev # 新的头节点
2. 力扣链表真题深度剖析
2.1 移除链表元素(203题)
这个问题要求删除链表中所有值等于给定val的节点。关键在于处理连续出现目标值和头节点就是目标值的情况。以下是优化后的实现:
python复制def removeElements(head, val):
dummy = ListNode(0)
dummy.next = head
current = dummy
while current.next:
if current.next.val == val:
current.next = current.next.next # 跳过目标节点
else:
current = current.next # 只有非目标值才移动指针
return dummy.next
注意事项:
- 使用虚拟头节点可统一处理头节点删除的情况
- 只有当current.next不是目标值时才移动current指针,避免连续目标值漏删
- 时间复杂度O(n),空间复杂度O(1)
2.2 设计链表(707题)
实现完整的链表类需要全面考虑各种边界条件。下面是增强版的实现,包含详细的错误处理:
python复制class ListNode:
def __init__(self, val=0):
self.val = val
self.next = None
class MyLinkedList:
def __init__(self):
self.dummy = ListNode(0) # 永久虚拟头节点
self.size = 0
def get(self, index):
if index < 0 or index >= self.size:
return -1
current = self.dummy.next
for _ in range(index):
current = current.next
return current.val
def addAtHead(self, val):
self.addAtIndex(0, val)
def addAtTail(self, val):
self.addAtIndex(self.size, val)
def addAtIndex(self, index, val):
if index < 0 or index > self.size:
return
prev = self.dummy
for _ in range(index):
prev = prev.next
new_node = ListNode(val)
new_node.next = prev.next
prev.next = new_node
self.size += 1
def deleteAtIndex(self, index):
if index < 0 or index >= self.size:
return
prev = self.dummy
for _ in range(index):
prev = prev.next
prev.next = prev.next.next
self.size -= 1
关键改进点:
- 使用永久虚拟头节点简化所有操作
- addAtHead和addAtTail复用addAtIndex实现
- 严格校验index范围,避免非法访问
- 维护size变量确保O(1)时间获取长度
2.3 反转链表(206题)
递归和迭代是解决反转问题的两种经典方法。迭代法已在技巧部分介绍,下面是递归实现:
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)的空间复杂度(调用栈)。在实际面试中,建议先给出迭代解法,再视情况补充递归实现。
3. 进阶链表问题实战
3.1 两两交换节点(24题)
这个问题要求每两个相邻节点进行交换,使用虚拟头节点可以大幅简化操作:
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 = first
return dummy.next
易错点分析:
- 循环条件必须同时检查prev.next和prev.next.next
- 交换后需要正确更新prev指针位置
- 注意处理链表长度为奇数时的剩余节点
3.2 删除倒数第N个节点(19题)
双指针法的经典应用,让快指针先走N步,然后同步移动直到快指针到达末尾:
python复制def removeNthFromEnd(head, n):
dummy = ListNode(0)
dummy.next = head
fast = slow = dummy
# 快指针先走n+1步
for _ in range(n + 1):
fast = fast.next
# 同步移动直到快指针到末尾
while fast:
fast = fast.next
slow = slow.next
# 删除目标节点
slow.next = slow.next.next
return dummy.next
技巧:让快指针多走一步,可以使慢指针停在目标节点的前驱位置,方便删除操作。
3.3 链表相交问题(面试题02.07)
这个问题的关键在于消除两个链表的长度差。下面是优化后的实现:
python复制def getIntersectionNode(headA, headB):
p1, p2 = headA, headB
while p1 != p2:
p1 = p1.next if p1 else headB
p2 = p2.next if p2 else headA
return p1
这个精妙的解法通过让两个指针分别遍历两个链表,最终会在相交点相遇(或者同时到达None)。时间复杂度O(m+n),空间复杂度O(1)。
3.4 环形链表检测(142题)
Floyd判圈算法的典型应用,分为两个阶段:
python复制def detectCycle(head):
slow = fast = head
# 第一阶段:判断是否有环
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
break
else:
return None # 无环
# 第二阶段:寻找环入口
ptr = head
while ptr != slow:
ptr = ptr.next
slow = slow.next
return ptr
算法原理:
- 快慢指针相遇说明有环
- 相遇后,将其中一个指针移回头部,然后同速前进,再次相遇点即为环入口
- 数学证明:设头到入口距离a,入口到相遇点距离b,环长为L,则有2(a+b)=a+b+kL ⇒ a=(k-1)L+(L-b)
4. 链表问题通用解题框架
通过以上例题,可以总结出链表问题的通用解决框架:
- 明确问题类型:确认是基本操作、反转、环检测还是多链表处理
- 选择合适技巧:
- 单链表操作优先考虑虚拟头节点
- 涉及位置关系使用双指针
- 复杂问题尝试递归分解
- 处理边界条件:
- 空链表处理
- 头/尾节点特殊处理
- 单节点链表情况
- 验证指针移动:
- 确保指针不会访问None的next
- 注意指针移动顺序
- 复杂度分析:
- 通常要求O(n)时间,O(1)空间
- 递归解法需要说明栈空间开销
在面试实践中,建议先口头说明解题思路,明确边界条件后再开始编码。完成代码后,用测试用例(空链表、单节点、头尾节点等)验证正确性。对于复杂问题,可以分步骤实现,例如先解决无环情况再处理有环情况。