[数据结构与算法] 递归解题笔记

关于递归的本质理解,以及解题模板。

递归理解

​ 递归本质上类似于循环,只不过是通过循环体调用自己,来进行所谓的循环。

​ 为什么会有这样一种形式呢?就是因为计算机语言在创造的时候,本质上是汇编。汇编的特点就是没有所谓的循环嵌套这一说法,它的执行方式是,程序员之前在某个地方写了一段函数,或者一段指令,汇编执行就直接不断地反复跳转到之前的那段指令,不断去执行。这就是所谓的递归。

​ 循环本身可以被编译出来,再去看它的汇编代码。会发现,其实循环和递归本身有异曲同工之处。因此,递归和循环两者并没有明显的边界。

​ 重复性可以用计算机解决,从而给人类省很多事。

递归与《盗梦空间》

​ 找到递归的解题感觉,就是要找到所谓的归去来兮的感觉。可以和盗梦空间联系起来。

主线情节

​ 从飞机开始,然后去到城市,再往下不断递归,最后在雪山的屋子里,这时已经是第三层递归了。

每一层递归时间会变慢(这点暂且不考虑),每进到一层递归里,相当于进入一个新的世界,或者说一个互不干扰的世界,可以在里面做一些事情。再下到另外更深一层,再做另外一些事情。

​ 如果要返回现实世界,必须再一层一层的回来,而且每次下去或者回来的时候,自身状态会发生改变,或者把自身的状态带到下一层,发生改变后再带回来(类似于传引用)。但是环境里面的东西是不受影响的,也就是说下一层的环境不会影响这一层环境里面的人和物,除非是主角或者主角相关的人。我们可以把这些人(即主角团队)理解为函数的参数。

递归的特点

递归的特点是:

  1. 向下进入到不同的递归层,也就是所谓的梦境,向上又回到原来的层。一般来说不能直接跳跃,必须一层一层地下去,然后一层一层地回来。所谓的就是有一种对称性,或者叫做归去来兮的感觉。

  2. 通过声音同步回到上一层。所谓这种同步的关系,就是用参数来进行函数不同层之间的变量传递

  3. 每一层的环境和周围的人都是一份拷贝,每一层的房子和建筑等等,以及所谓的无关人物,都类似于硬生生的创造了一个新的世界。即使把这个世界整个都打坏了,当去到下一层或者回到上一层,上一个世界里面的建筑之类,还是不受影响。然而主角等几人团队,可以穿越不同层级的梦境,同时把自身和自己所要携带的东西,带到不同的梦境中去发生变化,并把变化携带回来。主角团队类似于函数的参数,同时还会有一些全局变量。

  4. 递归最后的运行方式,就成了一个递归栈。一层一层展开,这种形式就像一种剥洋葱的形式。所谓剥洋葱的形式,就是类似于一个栈的形式一层一层一层进去,然后把它剥开。而栈本身就是递归调用的时候,系统给我们自动生成了一个这样的调用栈

note:做递归时,想到盗梦空间,该怎么弄。

递归模板

1. recursion terminator(递归终结者)

​ 也就是在写递归函数开始,一定要记得先把函数的递归终止条件写上。否则,会造成无限递归(死递归),最后只能把程序杀掉。

2. process logic in current level (处理当前层逻辑)

​ 完成这一层要进行的逻辑代码。

3. drill down(下探到下一层)

​ 需要用参数标记当前是哪一层。并且把参数放进去。

4. reverse the current level status if needed(清理当前层)

​ 如果递归完了,最后一部分这一层有些东西可能要清理。

思维要点

  1. 抵制人肉递归。

    不要进行人肉递归,即用人类的思维去一个一个枚举和数数。前期可以画出递归树,但后期要逐渐养成直接看函数本身就开始写。

  2. 找最近重复子问题。

    找到最近最简方法,将其拆解成可重复解决的问题(重复子问题)。因为我们写的程序指令,只包括if else (判断)for-loop (循环),以及**recursion (递归调用)**这三部分。

    为什么看起来很复杂的逻辑可以用5到10行代码解决,就是因为问题本身具有可重复性。而计算机最擅长计算具有重复性的问题。

  3. 数学归纳法思维

    从n成立推到出n+1也成立。没有反例即正确。

实战

LeetCode-144

原代码:

class Solution {
public:
    vector preorderTraversal(TreeNode* root) {
        vector res;
        if (!root) {
            return res;
        }
        helper(root, res);
        return res;
    }

    void helper(TreeNode* root,vector &res){
        res.push_back(root->val);
        if (root->left) {
            helper(root->left, res);
        }
        if (root->right) {
            helper(root->right, res);
        }
    }
};

按照模板修改后代码:

class Solution {
 public:
    vector preorderTraversal(TreeNode* root) {
      vector res;
      helper(root, res);
      return res;
    }
  
    void helper(TreeNode* root,vector &res) {
      // 1 recursion terminator
      if (!root) {
        return;
      }
      
      // 2 process logic in current level
      TreeNode* left = root->left;
      TreeNode* right = root->right;
      
      // 3 drill down
      res.push_back(root->val);
      helper(left, res);
      helper(right, res);
      
      // 4 reverse the current level status if needed
    }
};

可见,按照模板写出的代码,条理更加清晰,并且还可简化代码(无需两次判断子节点的根节点是否为NULL,即原代码中2和3可以合并)。
注意:一定要养成机械化的记忆。一遇到递归问题,这4四部分啪啪啪的写出来。

实战题目

\70. 爬楼梯

\22. 括号生成

\226. 翻转二叉树

\98. 验证二叉搜索树

\104. 二叉树的最大深度

\111. 二叉树的最小深度

\297. 二叉树的序列化与反序列化

\236. 二叉树的最近公共祖先

\105. 从前序与中序遍历序列构造二叉树

\77. 组合

\46. 全排列

\47. 全排列 II

你可能感兴趣的:(笔记)