牛客网题目
1. 理解问题
给定一个单链表和两个整数 m
和 n
,要求反转链表中从位置 m
到位置 n
的节点,最后返回反转后的链表头节点。
示例:
-
输入:链表
1 -> 2 -> 3 -> 4 -> 5 -> NULL
,m = 2
,n = 4
-
输出:
1 -> 4 -> 3 -> 2 -> 5 -> NULL
关键点:
-
原地反转:要求空间复杂度为 O(1),可以有常量级别的中间值保存操作,。
-
边界条件:需要考虑
m
为 1(即从链表头开始反转)以及n
为链表长度的情况。 -
指针处理:需要正确处理反转部分前后的节点连接。
2. 输入输出
-
输入:
-
head
:链表的头节点。 -
m
、n
:指定反转的起始和结束位置,满足1 ≤ m ≤ n ≤ 链表长度
。
-
-
输出:
- 反转指定区间后的链表头节点。
3. 链表结构
链表节点的定义如下:
struct ListNode {
int val; // 节点的值
struct ListNode *next; // 指向下一个节点的指针
};
4. 制定策略
为实现指定区间的反转,需要以下步骤:
-
定位到反转起始位置的前一个节点:
-
使用一个辅助的
dummy
节点指向链表头,方便处理m = 1
的情况。 -
通过遍历,找到位置在
m - 1
的节点prev
。
-
-
反转从位置
m
到n
的子链表:-
初始化指针:
-
start
:指向位置m
的节点,反转后的尾部节点,即反转部分的第一个应该是反转后的最后一个。 -
then
:指向位置m + 1
的节点,反转操作中的当前节点。
-
-
反转过程:
- 将
then
节点逐个移动,并调整指针连接。
- 将
-
-
重新连接反转后的子链表:
- 反转完成后,
prev
的next
指针指向新的子链表头节点(即反转部分),start
的next
指针指向反转部分之后的节点。
- 反转完成后,
5. 实现代码
5.1 关键函数实现
struct ListNode* reverseBetween(struct ListNode* head, int m, int n) {
if (head == NULL) return NULL;
struct ListNode dummy;
dummy.next = head;
struct ListNode* prev = &dummy;
// 移动 prev 到位置 m-1
for (int i = 1; i < m; i++) {
prev = prev->next;
}
struct ListNode* start = prev->next;
struct ListNode* then = start->next;
// 反转位置 m 到 n 的节点
for (int i = 0; i < n - m; i++) {
start->next = then->next; //要反转的第一个数值start往后移
then->next = prev->next; //当前数值的后面是m-1的后面那个数
prev->next = then; //m-1数值的后面是当前数值
then = start->next; //当前数值往后移
}
return dummy.next;
}
假如m=2,n=4:
第一次循环(i = 0):
dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> NULL
^ ^ ^
prev start then
第二次循环(i = 1):
dummy -> 1 -> 4 -> 3 -> 2 -> 5 -> NULL
^ ^ ^
prev start then
5.2 完整的 C 语言代码
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点
struct ListNode {
int val;
struct ListNode *next;
};
// 反转指定区间的函数
struct ListNode* reverseBetween(struct ListNode* head, int m, int n) {
if (head == NULL) return NULL;
struct ListNode dummy;
dummy.next = head;
struct ListNode* prev = &dummy;
// 移动 prev 到位置 m-1
for (int i = 1; i < m; i++) {
prev = prev->next;
}
struct ListNode* start = prev->next;
struct ListNode* then = start->next;
// 反转指定区间
for (int i = 0; i < n - m; i++) {
start->next = then->next;
then->next = prev->next;
prev->next = then;
then = start->next;
}
return dummy.next;
}
// 创建新节点的辅助函数
struct ListNode* createNode(int value) {
struct ListNode* newNode = (struct ListNode*)malloc(sizeof(struct ListNode));
if (!newNode) {
perror("创建节点失败");
exit(EXIT_FAILURE);
}
newNode->val = value;
newNode->next = NULL;
return newNode;
}
// 打印链表的辅助函数
void printList(struct ListNode* head) {
struct ListNode* current = head;
while (current != NULL) {
printf("%d", current->val);
if (current->next != NULL) {
printf(" -> ");
}
current = current->next;
}
printf(" -> NULL\n");
}
// 释放链表内存
void freeList(struct ListNode* head) {
struct ListNode* current = head;
struct ListNode* temp;
while (current != NULL) {
temp = current->next;
free(current);
current = temp;
}
}
// 测试代码
int main() {
// 创建链表 {1 -> 2 -> 3 -> 4 -> 5}
struct ListNode* head = createNode(1);
head->next = createNode(2);
head->next->next = createNode(3);
head->next->next->next = createNode(4);
head->next->next->next->next = createNode(5);
printf("原始链表:\n");
printList(head);
int m = 2, n = 4;
struct ListNode* modifiedHead = reverseBetween(head, m, n);
printf("反转第 %d 到 %d 个节点后的链表:\n", m, n);
printList(modifiedHead);
// 释放内存
freeList(modifiedHead);
return 0;
}
5.3 代码说明
-
reverseBetween 函数:
-
dummy 节点:创建一个哑节点
dummy
,指向链表头head
,用于简化边界条件的处理。 -
prev 指针:初始化为
dummy
,用于定位到反转区间的前一个节点。 -
start 和 then 指针:
start
指向反转区间的第一个节点,then
指向需要反转的下一个节点。 -
反转过程:通过调整指针的指向,逐步将
then
节点插入到prev
后面,实现局部反转。
-
-
辅助函数:
-
createNode:创建新节点,初始化节点的值和指针。
-
printList:遍历并打印链表的所有节点值。
-
freeList:释放链表占用的内存,防止内存泄漏。
-
-
主函数 main:
-
创建示例链表并输出。
-
调用
reverseBetween
函数反转指定区间的节点。 -
输出反转后的链表结果。
-
释放链表内存。
-
5.4 运行结果
原始链表:
1 -> 2 -> 3 -> 4 -> 5 -> NULL
反转第 2 到 4 个节点后的链表:
1 -> 4 -> 3 -> 2 -> 5 -> NULL
6. 时间和空间复杂度分析
-
时间复杂度:O(n)
- 需要遍历链表一次,找到位置
m
,然后进行n - m
次反转操作。
- 需要遍历链表一次,找到位置
-
空间复杂度:O(1)
- 只使用了常量级的额外空间,主要是几个指针变量。
7. 总结
通过上述方法,我们成功实现了对单链表指定区间的节点进行反转的操作。关键在于正确定位需要反转的区间,并在反转过程中维护好前后节点的连接关系。
此方法不仅有效解决了本题,还可以应用于其他涉及链表部分反转或节点调整的题目。熟练掌握链表操作和指针的使用,对于处理类似问题至关重要。