二叉树与链表中的递归思想

二叉树与链表中的递归思想

对很多同学而言,递归可能是在计算机编程入门过程中的一道屏障,我们可以很快的理解for/while 迭代,看到递归却感到头大,这是一个正常的现象,因为在迭代中,解决问题的顺序是很明了的,但是在递归中,这个顺序却变得模糊起来.

就拿最简单的1~n求和来举例

// 迭代版本
public int getSum(int n){
    int sum = 0;
	for(int i=1; i<=n; i++) sum += i;
	return sum;
}
// 递归版本
public int getSum(int n){
	if(n == 0) return 0;
	return n + getSum(n-1);
}

我们发现,用循环的思想解决问题就是理解初始情况和终止情况,然后明确每种情况间是如何变化的。

而递归其实很类似,就是理解不同规模问题是否存在共性,并明确规模最小时的情况,以及较大规模情况时如何从较小规模情况演变过来的

在理解了递归的思想后,不如回到我们的主题,链表,二叉树和递归有什么关系呢?

链表和二叉树的某一部分一直都会是该结构的子结构,而这与之前的递归思想不谋而合

  1. 反转链表 Leetcode 206
    在这里插入图片描述
// 迭代版本 头插法
/* 
 newHead -> null
 newHead -> 1 -> null
 newHead -> 2 -> 1 -> null
 ...
 newHead -> 5 -> 4 -> 3 -> 2 -> 1 -> null
*/
class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode newHead = new ListNode(-1);
        while (head != null) {
            ListNode next = head.next;
            head.next = newHead.next;
            newHead.next = head;
            head = next;
        }
        return newHead.next;
    }
}

// 递归版本
/*
递归是从简单的情况开始解决,假设我们当前已经解决了3个节点,
然后我们要解决4个节点的问题
当前情况为 2 -> 5 -> 4 -> 3, 转化为 5 -> 4 -> 3 -> 2
*/
class Solution {
	public 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;
	}
}
/*作者:LeetCode
链接:https://leetcode-cn.com/problems/reverse-linked-list/solution/fan-zhuan-lian-biao-by-leetcode/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。*/

递归和迭代并非完全对立,他们只是解决一个问题的两种不同的模拟方式,在某些场景下,迭代和递归也要进行适当的结合

  1. 从链表中删去总和值为0的连续节点 Leetcode 1171
    在这里插入图片描述
    如果我们分析这道题要做的事情,既不是从头部删除和为0的部分,也不是从链表中删除一些连续的节点,而是从链表中删除所有和为0的连续节点,这个连续的部分可能从链表的任意部分开始,也可能有任意数量这样的连续节点群。

在这种情况下,如果只使用递归,那么一定需要一个辅助递归函数

private ListNode removeTargetValue(ListNode head, int target);

但是这样会令我们的递归过程变得复杂且不够直观,下面是递归方法

class Solution {
    private boolean found = false; // 记录是否找到和为0连续节点
    public ListNode removeZeroSumSublists(ListNode head) {
        if(head == null) return head;
        ListNode headPrev = new ListNode(-1);
        headPrev.next = head;
        while(true){
            head = removeTargetValue(head,0);
            found = false;
            if(headPrev.next == head) break;
            else headPrev.next = head;
        }
        /* 当前节点开始已经无和为0的连续部分,从下一个节点开始继续递归重复整个过程 */
        if(head != null)
            head.next = removeZeroSumSublists(head.next);
        return headPrev.next;
    }
    private ListNode removeTargetValue(ListNode head, int target){
        if(head == null) return null;
        if(head.val == target){
            found = true;
            return head.next;
        }
        ListNode ans = removeTargetValue(head.next, target-head.val);
        /* 如果找到和为0连续节点,返回该连续部分的下一个节点,反之返回原节点 */
        return found ? ans : head; 
    }
}

如果我们在和是否为0这个过程中使用迭代,而在选择连续部分开始节点的过程中使用递归,则并不需要辅助函数,代码也相对简洁直观,下面是递归 + 迭代

class Solution {
    public ListNode removeZeroSumSublists(ListNode head) {
        if(head == null) return head;
        ListNode headPrev = new ListNode(-1);
        ListNode headCopy = head; /* 记录当前头节点,因为后续迭代部分会更新头节点 */
        headPrev.next = headCopy;
        int sum = 0;
        while(head != null){
            sum += head.val;
            head = head.next;
            if(sum == 0){
            /* 如果有以头节点为开始节点的连续为0部分,直接将头节点更新为该连续部分的下一个节点
            并继续递归*/
                headPrev.next = removeZeroSumSublists(head);
            }
        }
        /* 若迭代过程中未找到以头节点为开始节点的连续为0部分,
        则从下一个节点开始递归重复这一过程 */
        if(headPrev.next == headCopy){
            head = headCopy;
            headCopy.next = removeZeroSumSublists(head.next);
        }
        return headPrev.next;
    }
}

看完了链表,让我们再处理一下二叉树,同学们会不会感觉更加得心应手了呢?

  • 路径总和 Leetcode 437
    在这里插入图片描述
    注意题目中的几个关键点,思考一下这些在代码中应该如何体现呢
  • 无须从根节点开始
  • 无须在叶节点结束
  • 路径是连续向下的节点
class Solution {
    public int pathSum(TreeNode root, int sum) {
        if(root == null){
            return 0;
        }
        /* dfs(TreeNode,int) 搜索了以当前节点为开始节点的所有路径,
        而pathSum(TreeNode,int)的递归则确保把所有节点作为开始节点 */
        return dfs(root, sum) + pathSum(root.left, sum) + pathSum(root.right, sum);
    }
    
    public int dfs(TreeNode root, int sum){
        int res = 0;
        if(root == null){
            return res;
        }
        /* 当找到一条路径时并不return,而是继续寻找直至叶节点,
        这样确保了一条线路上的所有可能性全部发掘
        e.g. sum = 3
        1 -> 2 -> 3 -> -3 这条线路上总共有2条路径而非1条 */
        if(root.val == sum){
            res++;
        }
        res += dfs(root.left, sum - root.val);
        res += dfs(root.right, sum - root.val);
        return res;
    }
}

总结

其实递归并非想象的那么难,它只是一种模拟过程的方式,在解决实际问题的过程中,只有我们真正理解问题是如何处理的,才能选择出适当的方法,在问题中有如下特性时,可以考虑选择递归的方式

  1. 问题采取的数据结构可以实践递归思想
  2. 问题拥有“规模”这一概念
  3. 不同规模间有一定的联系

理解了递归的思想,后续的 DFS算法 和 动态规划思想 也会变得有迹可循。

希望这篇文章对大家有所帮助!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值