关于递归的本质理解,以及解题模板。
递归本质上类似于循环,只不过是通过循环体调用自己,来进行所谓的循环。
为什么会有这样一种形式呢?就是因为计算机语言在创造的时候,本质上是汇编。汇编的特点就是没有所谓的循环嵌套这一说法,它的执行方式是,程序员之前在某个地方写了一段函数,或者一段指令,汇编执行就直接不断地反复跳转到之前的那段指令,不断去执行。这就是所谓的递归。
循环本身可以被编译出来,再去看它的汇编代码。会发现,其实循环和递归本身有异曲同工之处。因此,递归和循环两者并没有明显的边界。
重复性可以用计算机解决,从而给人类省很多事。
找到递归的解题感觉,就是要找到所谓的归去来兮的感觉。可以和盗梦空间联系起来。
主线情节:
从飞机开始,然后去到城市,再往下不断递归,最后在雪山的屋子里,这时已经是第三层递归了。
每一层递归时间会变慢(这点暂且不考虑),每进到一层递归里,相当于进入一个新的世界,或者说一个互不干扰的世界,可以在里面做一些事情。再下到另外更深一层,再做另外一些事情。
如果要返回现实世界,必须再一层一层的回来,而且每次下去或者回来的时候,自身状态会发生改变,或者把自身的状态带到下一层,发生改变后再带回来(类似于传引用)。但是环境里面的东西是不受影响的,也就是说下一层的环境不会影响这一层环境里面的人和物,除非是主角或者主角相关的人。我们可以把这些人(即主角团队)理解为函数的参数。
递归的特点是:
向下进入到不同的递归层,也就是所谓的梦境,向上又回到原来的层。一般来说不能直接跳跃,必须一层一层地下去,然后一层一层地回来。所谓的就是有一种对称性,或者叫做归去来兮的感觉。
通过声音同步回到上一层。所谓这种同步的关系,就是用参数来进行函数不同层之间的变量传递。
每一层的环境和周围的人都是一份拷贝,每一层的房子和建筑等等,以及所谓的无关人物,都类似于硬生生的创造了一个新的世界。即使把这个世界整个都打坏了,当去到下一层或者回到上一层,上一个世界里面的建筑之类,还是不受影响。然而主角等几人团队,可以穿越不同层级的梦境,同时把自身和自己所要携带的东西,带到不同的梦境中去发生变化,并把变化携带回来。主角团队类似于函数的参数,同时还会有一些全局变量。
递归最后的运行方式,就成了一个递归栈。一层一层展开,这种形式就像一种剥洋葱的形式。所谓剥洋葱的形式,就是类似于一个栈的形式一层一层一层进去,然后把它剥开。而栈本身就是递归调用的时候,系统给我们自动生成了一个这样的调用栈。
note:做递归时,想到盗梦空间,该怎么弄。
1. recursion terminator(递归终结者)
也就是在写递归函数开始,一定要记得先把函数的递归终止条件写上。否则,会造成无限递归(死递归),最后只能把程序杀掉。
2. process logic in current level (处理当前层逻辑)
完成这一层要进行的逻辑代码。
3. drill down(下探到下一层)
需要用参数标记当前是哪一层。并且把参数放进去。
4. reverse the current level status if needed(清理当前层)
如果递归完了,最后一部分这一层有些东西可能要清理。
抵制人肉递归。
不要进行人肉递归,即用人类的思维去一个一个枚举和数数。前期可以画出递归树,但后期要逐渐养成直接看函数本身就开始写。
找最近重复子问题。
找到最近最简方法,将其拆解成可重复解决的问题(重复子问题)。因为我们写的程序指令,只包括if else (判断) , for-loop (循环),以及**recursion (递归调用)**这三部分。
为什么看起来很复杂的逻辑可以用5到10行代码解决,就是因为问题本身具有可重复性。而计算机最擅长计算具有重复性的问题。
数学归纳法思维
从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