38.外观数列_递归_构造器理解

写在前

递归这真是费脑筋的事,有很多算法题都明确要求使用递归,所以是避免不了的,今天我要砍到这座大山,向前踏进一步,让自己不至于遇到难一点的递归没有方向感。
(本文 参考微信公众号为 labuladong)的 浅谈递归II 这篇文章。

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

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

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

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

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

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

我认为这个定义非常优秀,因为它在数学上确实能完成任务,不过稍作思考,我似乎从算法分析的角度发现问题:这个递归过程存在冗余计算。我这么说,你是不是觉得有些道理?不过很遗憾,如果你动笔写一下这个生成过程,发现它们是一样的,只要我们那个构造器每次选取做小的那个元素进行生成,而另一个每次选取最大的那个数生成就行了。
上面只是一个开胃菜,意在让读者有个 “构造器” 这样的概念,下面用这个思路来举两个算法问题。

实战1

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

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

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

这里先跳过这题,先看第二题

实战2

给一个正整数 n ,返回所有包含 n 个节点的合法的二叉树。比如 n = 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

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

  • 实战1 解答
        List<String> generateParenthesis(int n) {
            if (n == 0) return Arrays.asList(""); // 基本情况
            List<String> ans = new ArrayList<>();
            for (int i = 0; i < n; i++)
                for (String left : generateParenthesis(i)) // 挑选 s
                    for (String right : generateParenthesis(n - i - 1)) // 挑选 t
                        ans.add("(" + left + ")" + right); // 构造
            return ans;
        }

如果有问题,可以回头看下上面的文字分析,反复理解。

  • 实战2解答
        List<TreeNode> generateTrees(int n) {
            if (n == 0) return Arrays.asList();
            return helper(1, n);
        }

        List<TreeNode> helper(int lo, int hi) {
            if (lo == hi) return Arrays.asList(new TreeNode(lo));
            if (lo > hi) return Arrays.asList(null);
            List<TreeNode> res = new ArrayList<>();
            for (int i = lo; i <= hi; i++)
                for (TreeNode left : helper(lo, i - 1)) // 构造左子树
                    for (TreeNode right : helper(i + 1, hi)) { // 构造右子树
                        TreeNode cur = new TreeNode(i); // 组装
                        cur.left = left;
                        cur.right = right;
                        res.add(cur); // 加入解集
                    }
            return res;
        }

        class TreeNode {
            int val;
            TreeNode(int val){
                this.val = val;
            }
            TreeNode left;
            TreeNode right;
        }

实战3

  1. 外观数列。
    「外观数列」是一个整数序列,从数字 1 开始,序列中的每一项都是对前一项的描述。前五项如下:
    在这里插入图片描述
    说明:
		1 被读作  "one 1"  ("一个一") ,1111 被读作 "two 1s" ("两个一",2121 被读作 "one 2",  "one 1""一个二" ,  "一个一") ,1211。
		
		给定一个正整数 n(1 ≤ n ≤ 30),输出外观数列的第 n 项。
		
		注意:整数序列中的每一项将表示为一个字符串。
		
		来源:力扣(LeetCode)
		链接:https://leetcode-cn.com/problems/count-and-say

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值