构造函数的视角写递归,附 LeetCode 例题详解

递归这个方法很简单,很实用,不过需要花时间理解和练习。最好能从多方面来思考它,同时尽力应用到实际中去,这样有助于我们冒出一些有趣的奇思妙想。今天从数学上简单的递归生成函数推广到一类相似的问题。

递归可以这样拆分:基本情况(base case)和构造器(constructor),说起来玄之又玄,来举个例子,让我来生成一下所有自然数吧!我们可以这样递归地生成:

基本情况:规定 0 属于集合 S 。
构造器:如果一个数 x 属于集合 S ,那么 x + 1 也属于集合 S 。

这个很容易理解吧,就好比集合里放一个初始数,然后通过无限调用构造器,生成了整个自然数集合。如果要生成所有整数的集合呢?可以添加一个构造器:

第二个构造器:如果一个数 x 属于集合 S ,那么 -x 也属于集合 S 。

显然,通过这个构造器和第一个结合,就可以生成整个整数集合了。看到这里,你应该提问:凭什么要这样定义第二个构造器呢,

我定义一个:如果一个数 x 属于集合 S ,那么 x - 1 也属于集合 S 。

我认为这个定义非常优秀,因为它在数学上确实能完成任务,不过稍作思考,似乎从算法分析的角度发现问题:这个递归过程存在冗余计算。我这么说,你是不是觉得有些道理?但是它们本质是一样的,只需要采取不同的构造方式而已。只要我们那个构造器每次选取做小的那个元素进行生成,而另一个每次选取最大的那个数生成就行了。

上面只是一个开胃菜,意在让读者有个 “构造器” 这样的概念,下面用这个思路来举两个算法问题。

第一个问题很好理解:给一个正整数 n ,返回所有包含 n 对括号的合法括号序列。比如给 3 ,我们得返回这样一个序列(集合): ["((()))","(()())","(())()","()(())","()()()"] ,以此类推。

问题有点难度,显然要配合递归思想了,递归解题一定要用好数学归纳法(日后写一篇具体介绍),你这样想:如果我知道了规模为 n - 1 的问题的解,那么我如何解这个问题呢?我的讲解力求具体,这里具体来说就是:如果我知道了如何生成 n - 1 对括号的所有合法解的序列,如何生成 n 对括号的解?不知道。

下一步,加强归纳假设:假设我知道了如何生成任意 x (x <= n - 1) 对括号的所有合法解的序列,如何生成 n 对括号的解?这里似乎还是不好使,我就算知道了子问题的解,如何才能凑出原问题的解呢?问题在于规模为 n 的问题的解并不是规模为 n - 1 的子问题进行简单拼凑就能获得,而是要把这第 n 对括号在子问题里的解进行组合才行,怎么组合呢?

这里先跳过这题,看完第二题后就有点思路了:给一个正整数 n ,返回所有包含 n 个节点的合法的二叉树,比如 n = 3 就要返回下图:(题目和示例来自 LeetCode 中的 Unique Binary Search Trees II )

[
  [1,null,3,2],
  [3,2,null,1],
  [3,1,null,null,2],
  [2,1,3],
  [1,null,2,null,3]
]
   1         3     3      2      1
    \       /     /      / \      \
     3     2     1      1   3      2
    /     /       \                 \
   2     1         2                 3

用归纳法分析:假设我知道了如何生成任意 x (x <= n - 1) 个节点的所有合法二叉树的序列,如何生成 n 个节点的解?根据二叉树的基本结构构造:左子树 <- 当前节点 -> 右子树可以想到解法。

用具体例子解释下,比如说生成 3 个节点的所有合法二叉树,就有以下几种情况:根节点自己可以是 1, 2, 3,然后分析左右子树;可以左边挂 1 节点的二叉树,右边挂 1 节点的二叉树;或者左边 0 节点,右边 2 节点;或者左边 2 节点,右边 1 节点。这就是所有情况,刚才假设知道了如何生成任意 x (x <= n - 1) 个节点的所有合法二叉树的序列,所以以上分析的几种情况的解都是已知的。这道二叉树的题目略难,因为要控制取值边界,代码放最后,看懂括号生成的解法后有助理解。

继续讲括号生成,问题在于我们没办法像二叉树那样,有一个明确的构造器(左子树 <- 当前节点 -> 右子树)。那我们自己造一个(事实上是正确的):任何一个合法括号串都能分解为 [s]t 其中 s 和 t 都是合法括号串(规定空字符串也是合法的)。我们可以把这个规律设为构造器,运用归纳法:假设我知道了如何生成任意 x (x <= n - 1) 对括号的所有合法解的序列,如何生成 n 对括号的解?可以,generate(n) = “(” + generate(i) + “)” + generate(j) ,其中 i + j == n - 1 ,因为构造器里有一对括号。

类比刚才的数学问题,我们模仿一下,生成求解的集合 S:

基本情况:规定空串 "" 属于 S
构造器:S 中的任意两个串 s 和 t 这样组合得到 v = (s)t,v 也属于 S

你可以试一下,这样两条定义就可以生成所有合法括号串。按照这个逻辑基础,我们可以写代码了(别忘了之前说的,明确递归函数是干什么的):

vector<string> generateParenthesis(int n) {
    if (n == 0) return {""}; // 基本情况
    vector<string> ans;
    for (int i = 0; i < n; i++)
        for (string left : generateParenthesis(i)) // 挑选 s
            for (string right : generateParenthesis(n - i - 1)) // 挑选 t
                ans.push_back("(" + left + ")" + right); // 构造
    return ans;
}

应该不难理解,如果有问题,可以回头看下上面的文字分析。下面是二叉树的代码(重点是利用构造器的思路,如果实在搞不清取值问题,就算了):

vector<TreeNode*> generateTrees(int n) {
    if (n == 0) return {};
    return helper(1, n);
}

vector<TreeNode*> helper(int lo, int hi) {
    if (lo > hi) return { nullptr }; // base case
    vector<TreeNode*> res;
    for (int i = lo; i <= hi; i++)
        for (TreeNode* left : helper(lo, i - 1)) // 构造左子树
            for (TreeNode* right : helper(i + 1, hi)) { // 构造右子树
                auto cur = new TreeNode(i); // 组装
                cur->left = left;   
                cur->right = right; 
                res.push_back(cur); // 加入解集
            }
    return res;

总结:学习新东西后时时刻刻都要想着怎么用出来,数学、算法本身就是很多问题的抽象,学得好是一方面,怎么把抽象的东西实例化,应该时刻惦记着。

欢迎关注我的公众号,致力于把问题讲清楚:labuladong
公众号二维码

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值