文章目录
反转链表详解(C语言)
一、题目描述
给你单链表的头节点
head
,请你反转链表,并返回反转后的链表。
二、解题方法
1.头插法——创建一个新的链表
思路
直接创建一个新的头节点构成一个新链表,然后依次将原链表的每个节点按头插法插入新链表当中,利用头插法插入后节点顺序与原始输入顺序相反的特点,就达到了反转链表的目的。
具体操作
- 创建一个新的头节点
newHead
,定义两个指针变量:cur
用来遍历原链表各个节点、temp
用来记录cur
的下一个节点,防止找不到原链表以及返回反转后的新链表。- 依次将原链表各个节点头插到新链表,直到
cur
遍历到原链表结尾NULL
。- 最后用
temp
记录新链表的第一个节点,即newHead->next
,释放newHead
,返回temp
。
AC代码(附注释)
struct ListNode* reverseList(struct ListNode* head) { // 给这个结构体起一个别名,方便后续使用 typedef struct ListNode Node; //创建新链表 Node* newHead = (Node*)malloc(sizeof(Node)); newHead->next = NULL; //创建两个指针变量 Node* cur = head;//cur用来遍历原链表 Node* temp;//temp用来暂存节点 //遍历原链表,依次将节点头插至新链表 while (cur != NULL) { //先将当前节点cur的下一个节点暂存到temp中,因为接下来要改变cur->next的指向 temp = cur->next; //头插法 cur->next = newHead->next; newHead->next = cur; //将cur指针向后移动一位,指向下一个要处理的原链表节点 cur = temp; } //反转完成,将temp指向新链表真正的头节点 temp = newHead->next; //释放内存 free(newHead); //返回反转后的链表 return temp; }
**实际上,再定义一个新的链表,实现链表元素的反转,是对内存空间的浪费,其实只需要改变原链表next
指针的指向,直接将链表反转。**以下是几种实现方法:
2.迭代法——双指针实现
思路
用两个指针直接将原链表每个节点的
next
的指向反转。
具体操作
- 定义指针
cur
初始化为head
,指针prev
初始化为NULL
,再定义一个指针temp
用来暂存节点。- 先用
temp
保存cur
的下一个节点,然后让cur->next
的指向prev
,这样就反转一个节点了。- 不断移动
cur
和prev
执行上面的操作,直到cur
指向原链表的末尾NULL
。- 最后
prev
就指向了新的头节点,返回prev
即可。
AC代码(附注释)
struct ListNode* reverseList(struct ListNode* head) { //定义一个指针cur,初始时让它指向链表的头节点head,用于遍历链表 struct ListNode* cur = head; //定义一个指针prev,初始化为NULL,它将用于记录当前节点cur的前一个节点 struct ListNode* prev = NULL; //定义一个临时指针temp,用于暂存节点 struct ListNode* temp; //遍历链表,反转每个节点的next指向 while (cur != NULL) { //先将当前节点cur的下一个节点暂存到temp中,因为接下来要改变cur->next的指向 temp = cur->next; //将当前节点cur的下一个节点指向它的前一个节点prev,实现反转 cur->next = prev; //将prev指针更新为当前节点cur,因为此时cur已经完成了反转操作,成为了新的“前一个节点” prev = cur; //将cur指针向后移动一位,指向下一个要处理的链表节点 cur = temp; } //当遍历完整个链表后,prev指针将指向原来链表的最后一个节点,而这个节点在反转后成为了新的链表头节点 //所以返回prev,它就是反转后链表的头节点 return prev; }
3.递归法——从前往后反转
思路
我们可以通过迭代法得到递归法。
具体操作
- 创建一个递归函数
reverse
,用于反转链表,参数为prev
和cur
,与迭代法逻辑相同。- 初始参数为
NULL
和head
,相当于初始化prev
和cur
操作,当cur
为NULL
时,递归结束,返回prev
。- 每次递归先用
temp
暂存cur->next
,然后将cur->next
的指向反转,下一次递归传入参数为cur
和temp
,相当于迭代法中移动prev
和cur
的操作
AC代码(附注释)
struct ListNode* reverse(struct ListNode* prev, struct ListNode* cur) { // 如果当前要处理的节点cur为NULL,说明已经到达链表末尾,此时prev就是反转后的链表头节点 //直接返回prev即可 if (cur == NULL) { return prev; } //先将当前节点cur的下一个节点暂存到temp中,因为接下来要改变cur->next的指向 struct ListNode* temp = cur->next; //反转cur->next的指向 cur->next = prev; //相当于迭代法中的: // prev = cur; //cur = temp; return reverse(cur, temp); } struct ListNode* reverseList(struct ListNode* head) { //相当于迭代法中的: //struct ListNode* prev = NULL; //struct ListNode* cur = head; return reverse(NULL, head); }
这个递归写法和迭代法实质都是从前往后翻转指针指向,其实还有一种与迭代法思路不同的递归写法:从后往前翻转指针指向。
4.递归法Ⅱ——从后往前反转
思路
先处理链表为空或仅有一个节点的特殊情况,然后通过递归地反转子链表,并逐步调整节点间的连接关系,最终完成整个链表的反转,并返回反转后的链表头节点。
具体操作
- 如果链表为空或者只有一个节点,直接返回,无需反转。
- 递归调用
reverseList
函数,传入head->next
,将返回值保存在newHead
中,这样就可以先反转以head->next
为头节点的子链表,得到反转后的子链表的头节点newHead
。- 在递归回溯过程中,当到达链表的最后一个节点时,会开始从链表的尾部开始反转操作。
- 将当前节点
head
的下一个节点的下一个节点指向当前节点head
,即head->next->next = head
,实现指针的反转操作 ,将当前节点head的下一个节点设置为NULL
,因为完成一次反转后,原来的head节点在反转后的链表中就变成了尾节点。- 最后返回
newHead
,即反转后链表的头节点。
AC代码(附注释)
struct ListNode* reverseList(struct ListNode* head) { // 如果链表为空(即head指针为NULL),或者链表只有一个节点(即head的下一个节点NULL) // 在这两种情况下,链表无需反转,直接返回原链表的头节点head即可 if (head == NULL || head->next == NULL) { return head; } //递归调用reverseList函数,传入head的下一个节点head->next //先反转以head->next为头节点的子链表部分 //递归调用后,会得到反转后的子链表的头节点 struct ListNode* newHead = reverseList(head->next); //完成反转 head->next->next = head; //尾节点 head->next = NULL; //返回反转后的链表的头节点 return newHead; }
5.原地反转
思路
通过迭代的方式,利用两个指针
begin
和end
,在循环过程中逐步调整链表节点之间的连接关系,将节点依次从原链表的顺序反转过来,最终得到反转后的链表头节点并返回。
具体操作
- 先判断传入的链表头节点
head
是否为空。如果head
为空,这意味着链表本身就是空链表,无需进行反转操作,直接返回head
即可。- 定义两个指针
begin
和end
,begin
指针初始化为链表的头节点head
,end
指针初始化为头节点head
的下一个节点,它是接下来要移动和操作的节点,通过对end
节点的处理来逐步实现链表的反转。- 先将
begin
节点的下一个节点指向end
节点的下一个节点,防止后面链表丢失;接着将end
节点的下一个节点指向原来的头节点head
,实现了将end
节点插入到链表头部的操作;然后将head
指针更新为end
指针所指向的节点;最后将end
指针更新为begin
节点的下一个节点;- 循环结束后,意味着已经遍历完整个链表并完成了所有节点的反转操作。此时
head
指针所指向的节点就是反转后的链表头节点,直接返回head
即可。
AC代码(附注释)
struct ListNode* reverseList(struct ListNode* head) { //如果链表的头节点为空(即链表为空链表),那么直接返回这个空的头节点 if (head == NULL) { return head; } //定义一个指针begin,初始时让它指向链表的头节点head,它将作为反转过程中当前处理的起始节点 struct ListNode* begin = head; //定义一个指针end,初始时让它指向头节点head的下一个节点,它将作为反转过程中要移动和处理的节点 struct ListNode* end = head->next; while (end != NULL) { //先将begin节点的下一个节点指向`end`节点的下一个节点,防止后面链表丢失 begin->next = end->next; //将end节点的下一个节点指向原来的头节点head,实现了将end节点插入到链表头部的操作 end->next = head; // 将head指针更新为end指针所指向的节点 head = end; //将end指针更新为begin节点的下一个节点,以便继续处理下一个要反转的节点 end = begin->next; } //返回新的链表头节点head return head; }
6.栈
思路
利用一个模拟栈的结构来暂存链表中的节点,然后再从栈中依次取出节点重新构建链表,从而实现链表的反转。
具体操作
- 先处理特殊情况:链表为空或只有一个节点,直接返回
head
。- 计算链表长度,创建一个结构体指针数组
stack
模拟栈,将链表每个节点依次压入栈。- 先取出栈顶节点(也就是原链表的最后一个节点),将其赋值给
ans
,这个节点将作为反转后链表的头节点。同时,将临时指针temp
也初始化为ans
,用于后续在构建反转链表过程中连接其他从栈中取出的节点。- 然后从栈中依次取出其他节点并连接到反转链表中。
- 最后将最后一个节点的
next
指向NULL
,就反转完成了。直接返回ans
即可,如果是用malloc
动态分配的内存,别忘了手动释放stack
。
AC代码(附注释)
struct ListNode* reverseList(struct ListNode* head) { //处理特殊情况:链表为空或只有一个节点,直接返回head if (head == NULL || head->next == NULL) { return head; } //使用typedef将struct ListNode重命名为Node,方便后续代码书写 typedef struct ListNode Node; //计算原链表大小 int size = 0; Node* temp = head; while (temp != NULL) { size++; temp = temp->next; } //根据链表节点个数动态分配内存,创建一个数组来模拟栈,用于存储链表节点指针 Node** stack = (Node**)malloc(sizeof(Node*) * size); //栈顶指针,初始化为 -1,表示栈为空的初始状态 int top = -1; //将链表的每个节点依次压入栈中 while (head != NULL) { stack[++top] = head; head = head->next; } //从栈中取出栈顶节点(原链表的最后一个节点)作为反转后链表的头节点 Node* ans = stack[top--]; //从栈中依次取出其他节点并连接到反转链表中 temp = ans; while (top >= 0) { temp->next = stack[top--]; temp = temp->next; } //确保反转后链表的最后一个节点的next指针为NULL,形成正常的单链结构 temp->next = NULL; //释放之前为存储链表节点指针而动态分配的栈内存空间,避免内存泄漏 free(stack); //返回反转后链表的头节点 return ans; }