刚刚开始刷LeetCode的小白,决定用博客记录自己的刷题经历,监督自己吃透每一道题,尽量用简单明了的文字表述清楚解题步骤。
反转链表有许多种方法,迭代,递归等方法都可以实现。
笔者做题时主要是对迭代的步骤难以想清楚,翻看许多题解,一般讲解地没有那么详细,于是自己对照代码把每一步都画了一遍,终于理清了这个流程。想到可能还有许多小伙伴困于同样的问题,于是决定写下这篇博客记录下来,供大家参考,笔者水平有限,会尽量详细地记录自己的理解,如果有理解错误或表述不清的地方,欢迎大家指正讨论。
接下来让我们把每一步都画出来,好好剖析一下每一步的具体操作。
简单思路:每个函数内只包含判断是否到递归基,修改两个节点间的指向,调用自身和返回指针的功能。
代码如下:
ListNode* reverseList(ListNode* head) {
if(head==NULL || head->next==NULL) return head;
ListNode* p=reverseList(head->next);
head->next->next=head;
head->next=NULL;
return p;
}
分步过程:
PS:题目中画了5个节点,这里为了节省篇幅
和笔者时间,只画4个,大家注意力放在理解过程上就好。
图示中的各种表示形式 | 意义 |
---|---|
不同颜色的代码块 | 位于不同层次的函数中 |
标黄色块突出显示的语句 | 此步骤正在执行的语句 |
字体变绿的语句 | 已经执行完毕的语句 |
每幅图左上角的数字 | 第一个表示在第几层函数中,第二个表示在该函数中执行的第几步 |
1-2:此时头指针的值为1,非空;头指针的next的值为2,也非空。if判断语句的条件不成立,将不会执行后面的return。
1-3:此时在这个蓝色函数中定义一个p指针,递归调用自身,此后该层蓝色函数将不会再往下执行,而是会进入到被调用的那个(紫色)函数中去,直到那个(紫色)函数执行完毕才会接着执行这个蓝色函数。
2-1:现在进入紫色函数的这一层。由于传入的参数是“head->next”,于是这层紫色函数中的head指针相比与上层蓝色函数的head就往后移了一个节点。
这也正好体现了在不同层级的递归函数中,即使是名字相同的变量,实际上也可能是不同的。而其实这些变量由于所在的不同函数层级的生命周期不同,这些变量其实并没有同时存在过。前面的函数在遇到递归调用后,会被压入一个栈中,相当于被冻结起来了,直到与之相邻的后面那个递归调用函数执行完毕,它才会被弹出栈重新恢复运行,在写代码和解题过程中要特别注意这一点。
2-2:此时头指针的值为2,非空;头指针的next的值为3,也非空。if判断语句的条件不成立,将不会执行后面的return。
3-1:现在进入橙色函数的这一层。这层橙色函数中的head指针相比与上层紫色函数的head再往后移了一个节点。
3-2:此时头指针的值为3,非空;头指针的next的值为4,也非空。if判断语句的条件不成立,将不会执行后面的return。
4-1:这层红色函数中的head指针相比与上层橙色函数的head再往后移一个节点。
4-2:此时头指针的值为4,非空;头指针的next的值为空。if判断语句的条件成立,将执行后面的return。
4-3:执行return head语句,返回此时的head指针。该红色函数中后面的代码将不会再执行,该红色函数的生命周期结束,红色函数中所有的存储空间都将被释放,红色函数的head指针和p指针都不再存在。该红色函数仅返回一个指向4节点的head指针给上层的橙色函数。
3-3:故橙色函数中的p指针将被赋值为红色函数所返回的4节点。红色函数开始接着往下执行。
3-4:head指针的next为4节点,该句的意思即为4节点指向head指针所指向的节点,即3节点。
3-5:head指针的next指向空,即将3节点指向4的箭头去掉。
3-4和3-5两步完成了指针指向的反转。注意顺序,该顺序不可逆。
3-6:返回橙色函数中的p节点,至此,橙色函数也全部执行完毕,内存空间全部释放。
其实此时我们可以看出p指针就变成了整个反转后链表的头指针,指向反转后链表的第一个节点,这也是不会变了的,故每次递归函数都将返回p指针,递归函数中也不会修改p指针,递归函数中的所有的改变都由head指针完成。记住此点也许能避免一些不必要的错误。
2-3:紫色函数中的p指针被赋值为橙色函数的返回值,即那个不会改变了的头指针。
2-4:同上,紫色函数中的2节点的下一个(next)节点3节点指向2节点。
2-5:head指针指向的节点2节点指向空,即删除2节点指向3节点的箭头。
至此,2节点与3节点间的指向反转完成。
2-6:返回紫色函数中的p节点,至此,紫色函数也全部执行完毕,内存空间全部释放。
1-3:蓝色函数中的p指针被赋值为紫色函数的返回值,即那个不会改变了的头指针。
1-4:同上,蓝色函数中的1节点的下一个(next)节点2节点指向1节点。
1-5:head指针指向的节点1节点指向空,即删除1节点指向2节点的箭头。
1-6:函数的最后依然返回链表的头指针,至此,整个程序执行完毕,如图所示几乎全部语句变绿,除了部分if判断语句条件没满足时没有执行的return语句。
最后让我们整理一下我们等到的链表。
head指针在程序结束后会被释放,head指针也就不复存在。p指针也不再存在,只是程序最终会返回这个p指针,再看剩下的链表,完美反转~~完美!
写在最后:递归法会反复调用自身,时间和内存的消耗相对较大,程序写起来比较简单的代价是时间和空间的效率下降。想写出高效的代码还是要尽量减少递归的使用。
贴上别的方法(效率更高),不过本文就不再赘述了。
ListNode* reverseList(ListNode* head) {
ListNode* pre=NULL;
ListNode* cur=head;
while(cur)
{
ListNode* temp=cur->next;
cur->next=pre;
pre=cur;
cur=temp;
}
return pre;
}