92号题中递归的设计思路

来源:5-1 链表,在节点间穿针引线 Reverse Linked List

Poplar_hills

2019-05-09

bobo 老师,

这两天我在研究92号题目 Reverse Linked List II,在看到 Solution 中的 Approach 1: Recursion 时有如下问题想向您请教:

我能理解该解法的思路 —— 先将两个指针移动到数组 m, n 的位置上,再在他们互相逼近的过程中不断 swap 节点中的值。但是因为单向链表没有从后一个节点指向前一个节点的指针,因此若要让右边的指针左移到上一个节点就需要借助递归来实现,因为在每层递归结束回到上一层调用栈时可以获得上一个节点。

但即使理解了这个思路、看了答案中的代码,还是感觉没有吃透这道题,不知道这个递归是怎么设计出来的、为什么会这样设计。

我整理了一下,其中关键部分代码如下:

public ListNode reverseBetween(ListNode head, int m, int n) {
    this.left = head;
    this.stop = false;
    recurseAndReverse(head, m, n);
    return head;
}

private void recurseAndReverse(ListNode right, int m, int n) {
    if (n == 1) return;
    if (m > 1) this.left = this.left.next;
    right = right.next;

    recurseAndReverse(right, m - 1, n - 1);

    if (this.left == right || right.next == this.left)
        this.stop = true;
    if (!this.stop) {
        swapNodeValue(this.left, right);
        this.left = this.left.next;
    }
}

我试着分析了一下,在 recurseAndReverse 中:

  • 递归终止条件:n == 1

  • 递归最小单元:???

  • 进入下一层递归之前做的事:让两个指针向右移动,直到到达 m, n 上

  • 归回上一层递归之后做的事:判断两个指针是否已经撞上,若没有则交换节点值,并让两个指针互相接近一步(让右指针向左移动是由递归结束时原路返回上层(backtracking)实现的)。

想请问 bobo 老师:

  1. 递归的最小单元应该是哪部分?

  2. 递归逻辑是否应该这样分析?

  3. 在设计这个递归时应该怎么去想?感觉即使分析出了上面的四点,离最终写出这样的代码还是有很大距离。


非常感谢!

写回答

1回答

liuyubobobo

2019-05-10

大赞你的总结和认真:)


首先,我必须要说,如何设计一个递归算法,是没有一定之规的,也没有标准方式的。如果有一个标准方式,可以按照流程去操作,就能设计出递归算法,那么设计一个递归算法也就不是什么难事儿了,人人都能掌握它了。实际上,正是因为算法设计具有相当的灵活性,没有一定之规,所以,他才有难度,才有很多同学望而却步。


所以,最简单的方式,也是我最推荐的方式,是就到这里,不要再和这个问题死磕了。去见识更多的递归问题,对每个问题都这样总结,但千万不要陷入其中,无法自拔。见识的够多了,再找时间,再回头看这个问题,可能你会豁然开朗。可以参考我的公号文章,《高效学习的秘密》:https://mp.weixin.qq.com/s?__biz=MzU4NTIxODYwMQ==&mid=2247483836&idx=1&sn=90854aa76507281403e4dd9cd434a12b&chksm=fd8caefacafb27ec78f999fde4f1217c04c6e2ff28cf51fe511d8fa29d484d9281ff91de8c9c&token=847844479&lang=zh_CN#rd


==========


下面简单说一下我设计递归函数的"秘诀":


首先,我没有特别理解你说的递归最小单元是什么意思。在我看来,递归韩式只有两部分:

  1. 递归终止条件;

  2. 递归过程


写一个递归函数,将这两部分想清楚,就足够了。


对于递归终止条件,通常都是非常简单的,不多说。关键是递归过程。


我设计递归过程的秘诀,是定义清楚这个递归函数的语义。换句话说,就是,这个递归函数,输入是什么,输出是什么。


定义清楚之后,在递归过程中,你需要调用这个递归函数,但是,此时,不要把它想成一个递归函数,就把他想成一个普通函数。这个普通函数,有输入,有输出。关键是,使用这个普通的函数提供的输入和输出,如何构建你当前函数的逻辑。


不知道我这么说是不是比较抽象。


举一个最简单的例子,求一个二叉树的高度。代码大概是这样的:

class Solution {
public:
    int maxDepth(TreeNode* root) {
        
        // 递归终止条件
        if(root==NULL) 
            return 0;
        
        // 求左子树的高度
        int leftHeight = maxDepth(root->left);
        
        // 求右子树的高度
        int rightHeight = maxDepth(root->right);
        
        // 二者取最大值
        return max(leftHeight, rightHeight);
    }
};


注意一下注释,其实,这都是建立在,我们定义清楚了int maxDepth(TreeNode* root)这个函数,就是在求,以root为根节点的二分搜索树的高度上。我们内里的逻辑,直接使用这个函数的语义。这一点非常关键。我们调用递归函数的时候,不要去想这个函数的内部逻辑,把他当成一个已经封装好的函数,直接去调用。所谓的递归的宏观语义。我在《玩转数据结构》这门课程中,也特意强调过这个事情。


最后,对于这个问题,逻辑相对比较复杂,还有一个原因,是在够题目要求的one-pass这个条件。我的建议是,从理解递归的角度,先尝试实现two-pass的代码:)


这个课程后续,还会包含大量和递归相关的练习。树,图,回溯,动归,都离不开递归。如果一个问题想不透彻,不要止步不前。看更多的问题,回头再来看这个问题,很有可能豁然开朗。


依然是,一定要看一下这篇文章:https://mp.weixin.qq.com/s?__biz=MzU4NTIxODYwMQ==&mid=2247483836&idx=1&sn=90854aa76507281403e4dd9cd434a12b&chksm=fd8caefacafb27ec78f999fde4f1217c04c6e2ff28cf51fe511d8fa29d484d9281ff91de8c9c&token=847844479&lang=zh_CN#rd 


对于你的问题,尤其关注一下第八点:)


加油!:)

3
1
Poplar_hills
多谢 bobo 老师!
2019-05-10
共1条回复

玩转算法面试-- Leetcode真题分门别类讲解

课程配套大量BAT面试真题,高频算法题解析,强化训练

7410 学习 · 1150 问题

查看课程