1. 链表基础与问题概述
链表作为数据结构中的经典成员,在实际工程和算法面试中出现的频率仅次于数组。与数组的连续内存空间不同,链表通过指针将零散的内存块串联起来,这种非连续存储特性带来了插入/删除的高效性(O(1)时间复杂度),但也牺牲了随机访问能力(O(n)时间复杂度)。
今天要解决的三个问题恰好覆盖了链表操作的三个关键维度:
- 203.移除链表元素:基础操作能力检验,考察指针修改和边界处理
- 707.设计链表:系统化思维训练,需要完整实现增删查改接口
- 206.反转链表:经典指针操作题,涉及多指针协同工作
提示:链表问题80%的bug来源于指针丢失和边界条件未处理,建议在纸上画出节点和指针变化过程再写代码。
2. 203.移除链表元素实战解析
2.1 问题重述与示例分析
给定一个链表头节点和整数值val,删除链表中所有值为val的节点,返回新的头节点。例如:
code复制输入:1->2->6->3->4->5->6, val = 6
输出:1->2->3->4->5
2.2 虚拟头节点技巧
直接处理头节点需要特殊逻辑,引入dummy节点可统一操作:
python复制class Solution:
def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:
dummy = ListNode(next=head) # 创建虚拟头节点
curr = dummy
while curr.next:
if curr.next.val == val:
curr.next = curr.next.next # 跳过目标节点
else:
curr = curr.next
return dummy.next # 返回真实头节点
2.3 内存释放注意事项
在C++等需要手动管理内存的语言中,被删除的节点需要显式释放:
cpp复制ListNode* temp = curr->next;
curr->next = curr->next->next;
delete temp; // 释放内存
2.4 复杂度分析
- 时间复杂度:O(n),完整遍历一次链表
- 空间复杂度:O(1),仅使用常数级额外空间
3. 707.设计链表系统实现
3.1 需求规格说明
实现MyLinkedList类,包含以下功能:
- get(index):获取第index个节点的值
- addAtHead(val):头部插入节点
- addAtTail(val):尾部插入节点
- addAtIndex(index,val):指定位置插入节点
- deleteAtIndex(index):删除指定位置节点
3.2 双向链表实现方案
单链表实现查找需要O(n)时间,采用双向链表优化:
python复制class ListNode:
def __init__(self, val=0, prev=None, next=None):
self.val = val
self.prev = prev
self.next = next
class MyLinkedList:
def __init__(self):
self.head = ListNode()
self.tail = ListNode()
self.head.next = self.tail
self.tail.prev = self.head
self.size = 0
def get(self, index: int) -> int:
if index < 0 or index >= self.size:
return -1
curr = self.head.next
for _ in range(index):
curr = curr.next
return curr.val
def addAtHead(self, val: int) -> None:
self.addAtIndex(0, val)
def addAtTail(self, val: int) -> None:
self.addAtIndex(self.size, val)
def addAtIndex(self, index: int, val: int) -> None:
if index > self.size:
return
pred = self.head
for _ in range(index):
pred = pred.next
succ = pred.next
new_node = ListNode(val, pred, succ)
pred.next = new_node
succ.prev = new_node
self.size += 1
def deleteAtIndex(self, index: int) -> None:
if index < 0 or index >= self.size:
return
pred = self.head
for _ in range(index):
pred = pred.next
succ = pred.next.next
pred.next = succ
succ.prev = pred
self.size -= 1
3.3 边界条件处理要点
- 索引有效性检查(负数或超过长度)
- 空链表时的特殊处理
- 维护size变量的准确性
- 头尾哨兵节点的正确连接
4. 206.反转链表深度剖析
4.1 迭代法:三指针技巧
python复制def reverseList(head: ListNode) -> ListNode:
prev = None
curr = head
while curr:
next_temp = curr.next # 临时保存下一个节点
curr.next = prev # 反转指针方向
prev = curr # 前驱指针后移
curr = next_temp # 当前指针后移
return prev
4.2 递归法:理解调用栈
python复制def reverseList(head: ListNode) -> ListNode:
if not head or not head.next:
return head
p = reverseList(head.next)
head.next.next = head # 反转指针方向
head.next = None # 断开原指针
return p
4.3 复杂度对比
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 迭代法 | O(n) | O(1) | 内存受限环境 |
| 递归法 | O(n) | O(n) | 代码简洁优先场景 |
4.4 常见错误模式
- 指针丢失:未保存next节点就修改指针
- 循环链表:未正确处理尾节点指向
- 多节点跳过:移动指针时步长过大
5. 链表问题通用解题框架
5.1 解题四步法
- 画图分析:在纸上画出初始状态和每步变化
- 确定指针:明确需要几个指针参与操作
- 模拟运行:手动模拟前3-4步操作
- 边界检查:处理空链表、单节点、头尾节点等情况
5.2 调试技巧
- 打印链表辅助函数:
python复制def print_list(head):
curr = head
while curr:
print(curr.val, end=" -> ")
curr = curr.next
print("None")
- 使用小而具体的测试用例:
- 空链表
- 单节点链表
- 全相同值链表
- 头尾包含目标值的链表
5.3 性能优化方向
- 哨兵节点减少边界判断
- 双向链表优化查找效率
- 记录尾指针加速尾部操作
- 维护长度变量避免重复计算
在实际刷题过程中,我发现很多同学容易陷入"一看就会,一写就废"的困境。建议每个链表题目至少手写三遍:第一遍理解思路,第二遍闭卷实现,第三遍尝试不同解法。对于反转链表这样的经典题目,甚至可以隔周重复练习直到肌肉记忆形成。
