1. 项目概述
今天要分享的是LeetCode第92题"反转链表II"的C语言实现方案。这道题在链表操作类题目中属于中等难度,但非常考验对指针操作的熟练程度。我在面试中被问到过三次,第一次就栽了跟头,后来通过反复练习才掌握了其中的精髓。
这道题要求我们反转链表中从位置m到n的部分,其余部分保持不变。看似简单的需求,实际处理起来却有不少陷阱。比如当m=1时的边界情况处理、如何在不破坏链表结构的前提下完成局部反转等。经过多次实践,我总结出了一套稳定可靠的"一趟扫描+头插法"解决方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心需求解析
2.1 题目要求详解
给定一个单链表和两个整数m、n(1 ≤ m ≤ n ≤ 链表长度),要求反转链表中从第m个到第n个节点的部分。需要保证只遍历链表一次(一趟扫描),且空间复杂度为O(1)。
示例:
输入:1->2->3->4->5->NULL, m=2, n=4
输出:1->4->3->2->5->NULL
2.2 关键难点分析
- 边界处理:当m=1时,链表头节点会发生变化,需要特殊处理
- 指针操作:需要精确控制多个指针的指向关系,避免链表断裂
- 一趟扫描:要求在单次遍历中完成操作,不能使用额外空间存储节点
- 稳定性:算法需要能处理各种边界情况(如m=n,n等于链表长度等)
3. 解决方案设计
3.1 头插法原理
头插法是实现链表反转的经典方法,其核心思想是将需要反转的节点逐个插入到反转区间的前面。具体到本题:
- 定位到第m-1个节点(反转区间的前驱节点)
- 将第m+1到第n个节点逐个移动到第m-1个节点之后
- 最终形成反转后的链表结构
这种方法只需要维护几个指针变量,空间复杂度为O(1),且可以在一次遍历中完成。
3.2 一趟扫描实现思路
为了实现一趟扫描,我们需要:
- 使用一个dummy节点处理m=1的情况
- 维护pre指针指向反转区间的前驱节点
- 使用cur指针指向当前要处理的节点
- 通过next指针临时保存下一个节点
具体操作流程:
- 先遍历到第m-1个节点
- 然后对第m到第n个节点执行头插操作
- 最后调整指针连接整个链表
4. 代码实现详解
4.1 基础数据结构定义
首先定义链表节点结构:
c复制struct ListNode {
int val;
struct ListNode *next;
};
4.2 完整实现代码
c复制struct ListNode* reverseBetween(struct ListNode* head, int m, int n) {
if (head == NULL || m == n) return head;
// 创建dummy节点处理m=1的情况
struct ListNode dummy;
dummy.next = head;
struct ListNode *pre = &dummy;
// 移动到第m-1个节点
for (int i = 1; i < m; i++) {
pre = pre->next;
}
// 开始头插法反转
struct ListNode *cur = pre->next;
for (int i = m; i < n; i++) {
struct ListNode *next = cur->next;
cur->next = next->next;
next->next = pre->next;
pre->next = next;
}
return dummy.next;
}
4.3 关键代码解析
-
dummy节点的作用:
- 统一处理m=1和m≠1的情况
- 避免单独处理头节点变化的特殊情况
-
第一个for循环:
- 将pre指针移动到第m-1个节点位置
- 为后续头插操作确定插入位置
-
第二个for循环(核心反转逻辑):
- cur始终指向反转区间的第一个节点
- 将cur->next节点移动到pre->next位置
- 通过调整next指针完成插入操作
5. 执行过程图解
以输入1->2->3->4->5->NULL, m=2, n=4为例:
初始状态:
dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> NULL
pre指向1,cur指向2
第一次迭代:
将3移动到1后面:
dummy -> 1 -> 3 -> 2 -> 4 -> 5 -> NULL
cur仍指向2
第二次迭代:
将4移动到1后面:
dummy -> 1 -> 4 -> 3 -> 2 -> 5 -> NULL
完成反转
6. 复杂度分析
时间复杂度:O(n)
- 最坏情况下需要遍历整个链表一次
- 反转操作本身也是O(n)级别
空间复杂度:O(1)
- 只使用了固定数量的指针变量
- 没有使用递归或额外存储空间
7. 边界情况处理
7.1 m=1的情况
通过dummy节点统一处理,无需特殊逻辑
7.2 m=n的情况
直接返回原链表,开头已做判断
7.3 空链表情况
开头检查head==NULL直接返回
7.4 n等于链表长度
算法自动处理,无需额外判断
8. 常见错误与调试技巧
8.1 指针丢失问题
在反转操作时容易丢失对后续节点的引用。务必先保存next指针:
c复制struct ListNode *next = cur->next;
8.2 链表断裂问题
确保每一步操作后链表仍然是连通的。可以通过画图辅助理解指针变化。
8.3 边界条件测试
务必测试以下case:
- m=1, n=链表长度
- m=1, n=1
- 链表只有两个节点,m=1,n=2
- 链表只有一个节点
8.4 调试技巧
- 打印链表函数:
c复制void printList(struct ListNode *head) {
while (head) {
printf("%d ", head->val);
head = head->next;
}
printf("\n");
}
- 使用小规模测试用例逐步验证
9. 算法优化空间
9.1 代码简化
可以进一步合并一些指针操作,但会降低可读性
9.2 递归解法
虽然不符合题目空间要求,但可以作为练习:
c复制struct ListNode* reverseBetweenRecursive(struct ListNode* head, int m, int n) {
if (m == 1) {
return reverseN(head, n);
}
head->next = reverseBetweenRecursive(head->next, m-1, n-1);
return head;
}
struct ListNode* reverseN(struct ListNode* head, int n) {
if (n == 1) {
return head;
}
struct ListNode *newHead = reverseN(head->next, n-1);
struct ListNode *next = head->next->next;
head->next->next = head;
head->next = next;
return newHead;
}
10. 实际应用场景
链表反转操作在实际开发中有多种应用:
- 内存管理:某些内存池实现需要反转空闲块链表
- 数据同步:双向同步数据结构时可能需要局部反转
- 算法基础:是许多复杂算法的基础操作
- 面试考察:测试候选人对指针操作的掌握程度
11. 扩展练习建议
为了巩固链表操作能力,推荐尝试以下LeetCode题目:
-
- 反转链表(基础版)
-
- K个一组翻转链表(进阶版)
-
- 两两交换链表中的节点(变种)
-
- 重排链表(综合应用)
12. 个人实现心得
在实现这道题的过程中,我总结了几个关键点:
- 画图是理解链表操作的最佳方式,一定要在纸上画出指针变化过程
- dummy节点的使用可以极大简化边界条件处理
- 头插法虽然需要维护多个指针,但一旦理解就非常直观
- 测试用例要覆盖各种边界情况,特别是m=1和n=链表长度的情况
最开始的几次尝试中,我经常因为指针操作顺序错误导致链表断裂。后来发现按照以下顺序操作最可靠:
- 先保存next指针
- 调整cur的next指向
- 调整next节点的next指向
- 最后调整pre的next指向
这个顺序可以确保在任何时候都不会丢失对链表后续节点的引用。
