这是我参与「第五届青训营 」伴学笔记创作活动的第 32 天
这几天在力扣上刷了一些题,其中有一个误区我每次都误入,那就是递归问题。递归可以解决不少问题,最常见的就是链表的问题,但如何写出递归函数却成为了困难,我常常就写不对递归的位置,常常把每次的函数执行过程都在脑子中过一遍,但这样的方法显然是不可行的,执行次数太多容易犯迷糊,今天特地深入学习一下。
递归解题三部曲
何为递归?程序反复调用自身即是递归。
我自己在刚开始解决递归问题的时候,总是会去纠结这一层函数做了什么,它调用自身后的下一层函数又做了什么…然后就会觉得实现一个递归解法十分复杂,根本就无从下手。
相信很多初学者和我一样,这是一个思维误区,一定要走出来。既然递归是一个反复调用自身的过程,这就说明它每一级的功能都是一样的,因此我们只需要关注一级递归的解决过程即可。
如上图所示,我们需要关心的主要是以下三点:
- 整个递归的终止条件。
- 一级递归需要做什么?
- 应该返回给上一级的返回值是什么?
因此,也就有了我们解递归题的三部曲:
- 找整个递归的终止条件:递归应该在什么时候结束?
- 找返回值:应该给上一级返回什么信息?
- 本级递归应该做什么:在这一级递归中,应该完成什么任务?
一定要理解这3步,这就是以后递归秒杀算法题的依据和思路。
但这么说好像很空,我们来以题目作为例子,看看怎么套这个模版,相信通过例题,你就能慢慢理解这个模版。之后再解这种套路递归题都能直接秒了。
例:两两交换链表中的节点
看了一道递归套路解决二叉树的问题后,有点套路搞定递归的感觉了吗?我们再来看一道Leetcode中等难度的链表的问题,掌握套路后这种中等难度的问题真的就是秒:Leetcode 24. 两两交换链表中的节点
直接上三部曲模版:
- 找终止条件。 什么情况下递归终止?没得交换的时候,递归就终止了呗。因此当链表只剩一个节点或者没有节点的时候,自然递归就终止了。
- 找返回值。 我们希望向上一级递归返回什么信息?由于我们的目的是两两交换链表中相邻的节点,因此自然希望交换给上一级递归的是已经完成交换处理,即已经处理好的链表。
- 本级递归应该做什么。 结合第二步,看下图!由于只考虑本级递归,所以这个链表在我们眼里其实也就三个节点:head、head.next、已处理完的链表部分。而本级递归的任务也就是交换这3个节点中的前两个节点,就很easy了。
附上Java代码:
class Solution {
public ListNode swapPairs(ListNode head) {
//终止条件:链表只剩一个节点或者没节点了,没得交换了。返回的是已经处理好的链表
if(head == null || head.next == null){
return head;
}
//一共三个节点:head, next, swapPairs(next.next)
//下面的任务便是交换这3个节点中的前两个节点
ListNode next = head.next;
head.next = swapPairs(next.next);
next.next = head;
//根据第二步:返回给上一级的是当前已经完成交换后,即处理好了的链表部分
return next;
}
}
并不是所有链表问题都用递归
例如:反转链表_牛客题霸_牛客网 (nowcoder.com) 前几天在牛客网看到的一道题目,是链表反转。我当时就魔怔了,就想用递归,当时还不太会写递归,就一层一层往下想,头发都掉了一大把,就是他这个比较特殊。因为我们正常的递归都是先递推,再回归吗,返回的是第一层的结果,在这个题里就是返回原头节点,但它要求返回反转后的头节点,也就是原链表的尾节点,递归显然就不适用了。我哪能容忍,直接递归1.5次。先递推到尾节点,再将链表顺序反转。没想到居然还出来了。
源码
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode ReverseList(ListNode head) {
if(head == null) return null;
if(head.next == null) return head;
ListNode node = ReverseList(head.next);//拿到最后一个节点
Reverse(head);
//使指向方向改变,因为此函数返回的还是原头节点,题目要求返回新的头节点,所以放在另一个函数中
return node;
}
public ListNode Reverse(ListNode head) {//使指向方向改变,但返回的是原头节点
if(head.next == null) return head;
ListNode node = Reverse(head.next);
head.next = null;
node.next = head;
return head;
}
}
显然这是比较蠢的,然后看了评论区大神的解法
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
//评论区解法,不用递归
public class Solution {
public ListNode ReverseList(ListNode head) {
if(head==null)
return null;
//head为当前节点,如果当前节点为空的话,那就什么也不做,直接返回null;
ListNode pre = null;
ListNode next = null;
//当前节点是head,pre为当前节点的前一节点,next为当前节点的下一节点
//需要pre和next的目的是让当前节点从pre->head->next1->next2变成pre<-head next1->next2
//即pre让节点可以反转所指方向,但反转之后如果不用next节点保存next1节点的话,此单链表就此断开了
//所以需要用到pre和next两个节点
//1->2->3->4->5
//1<-2<-3 4->5
while(head!=null){
//做循环,如果当前节点不为空的话,始终执行此循环,此循环的目的就是让当前节点从指向next到指向pre
//如此就可以做到反转链表的效果
//先用next保存head的下一个节点的信息,保证单链表不会因为失去head节点的原next节点而就此断裂
next = head.next;
//保存完next,就可以让head从指向next变成指向pre了,代码如下
head.next = pre;
//head指向pre后,就继续依次反转下一个节点
//让pre,head,next依次向后移动一个节点,继续下一次的指针反转
pre = head;
head = next;
}
//如果head为null的时候,pre就为最后一个节点了,但是链表已经反转完毕,pre就是反转后链表的第一个节点
//直接输出pre就是我们想要得到的反转后的链表
return pre;
}
}
速度更快,更好理解,且代码更简洁明了。简直秒杀我的方法。
注释的很明白我就不解释了。总之不要一看到链表就递归,还是要就问题而论。
最后
递归是一种很有趣的方法,但只要你绕进去了就不有趣了,最重要的还是递归三部曲
- 找整个递归的终止条件:递归应该在什么时候结束?
- 找返回值:应该给上一级返回什么信息?
- 本级递归应该做什么:在这一级递归中,应该完成什么任务?