递归
阶乘
先用一个例子说明:
function factorial(n){
var result = n;
for(var i = n-1;i>0;i--){
result *= i;
}
return result;
}
上面使用循环实现了一个阶乘计算。如何换成递归呢?
如何求3
的阶乘?
答:需要求
2
的阶乘,再乘3
。
如何求2
的阶乘?
答:需要求
1
的阶乘,再乘2
。
如何求1
的阶乘?
答:需要求
1
的阶乘,1
的阶乘就是1。
这上面的都是重复的,让我们来考虑复杂点的:
如何求n
的阶乘?
如果值不是
1
,你就必须让n
和n-1
的阶乘相乘,可以简写成n! = n(n − 1)!
或者n * f(n-1)
,f(n-1)就是n-1的阶乘的数学写法。
function factorial(n){
if(n==1)
return 1;
return n*factorial(n-1);
}
当然,这两种方法求阶乘对于我们都能理解,可以理解为局部循环和函数循环,但都达到了一目的。
引自wiki百科(递归):
计算理论可以证明递归的作用可以完全替换循环。
汉诺塔
不太清楚怎么做的,可以先玩下游戏,然后再去实现。
假设有3个盘子,如何把这些盘子从左边柱子上,借助中间柱子移动到最右边?
需要将最上层两个盘子移动到中间柱子,然后将最下面的盘子移动到最右边,再将中间的两个盘子移动到最右边。
如何移动两个盘子?
需要将上面的盘子移动到中间的柱子,将第二个(底下的)盘子移动到目标盘子。
如果只有一个盘子?
直接移动盘子到目标柱子。
function hanoi(disc , src , mid , dst){
if(disc > 0){
hanoi(disc-1 , src , dst , mid);
console.log("Move the "+disc+" from "+src+" to "+dst);
hanoi(disc-1 , mid , src ,dst);
}
}
hanoi(3,a,b,c)
//Move the 1 from src to dst
//Move the 2 from src to mid
//Move the 1 from dst to mid
//Move the 3 from src to dst
//Move the 1 from mid to src
//Move the 2 from mid to dst
//Move the 1 from src to dst
分析:
盘子数不能小于1。
- 想要移动n个盘子,需要把上面的n-1个盘子移动到辅助柱子上,然后移动最底下的盘子到目标,最后,把辅助柱子上的盘子移动到目标柱子。
如果递归想不通,那么思考下循环,谁是第一个被移动的盘子(假设最底下是3号盘子)?
想移动3号盘子,需要移动2号盘子,想移动2号,盘子,需要移动1号盘子。所以在递归中,1号盘子先被移动。
1号盘子先被移动到那个柱子上呢?玩游戏我发现,单数盘子想要移动到目标柱子上,需要最顶层的盘子(1号盘子)先移动到目标柱子上;如果是双数盘子,则需要最顶层的盘子先移动到辅助柱子上。所以,到底1号盘子被先移动到那个柱子上,和盘子数有关。
从算法中我们亦可以看到,hanoi(disc-1 , src , dst , mid);交换了目标柱子和辅助柱子,如果disc为偶数,则1号(最顶层的)移动到了中间柱子(mid);如果disc为奇数,则1号盘子移动到了目标柱子(dst)。
画出移动盘子的树形图
这种访问顺序让我想起了中序遍历,但是和中序遍历有区别,它的每一层节点都是同一个节点。
中序遍历
//Definition for a binary tree node.
function TreeNode(val) {
this.val = val;
this.left = this.right = null;
}
//中序遍历
var inorderTraversal = function(root) {
var result = [];
var help = function(root){
if(root===null) return;
help(root.left);
result.push(root.val);
help(root.right);
}
help(root);
return result;
};
//对于这样一个二叉树(nu 是null)
1
/ \
2 3
/ \ / \
4 nu3 5 nu6
/ \ / \
nu1 nu2 nu4 nu5
分析:
help(值为1的节点);
help(值为2的节点)
help(值为4的节点);
help(值为nu1的节点);return;
result.add(4);
help(值为nu2的节点);return;
result.add(2);
help(值为nu3的节点);return;
result.add(1);
help(值为3的节点);
help(值为5的节点);
help(值为nu4的节点);return;
result.add(5);
help(值为nu5的节点);return;
result.add(3);
help(值为nu6的节点);return;
函数运行完毕。
这里,我们通过一个result变量来维持结果集,所有的子问题都返回结果给result变量。
我们想要得到这棵树的中序遍历,我们并没有上来就获取root的值,而是:
- 不停地访问他的左节点,直到null为止,然后获取最近的父节点的值。
- 然后再按照步骤1访问它的右节点。
这就是递归的力量:要想做整体的事情,只需要知道整体中一部分的解决办法就可以了(要知道何时停止)。
尾递归
再补充最后一个点,刚好还是一个尾递归。
像之前的递归阶乘就是一种尾递归。
摘自wiki:
尾调用的重要性在于它可以不在调用栈上面添加一个新的堆栈帧——而是更新它,如同迭代一般。尾递归因而具有两个特征:
1. 调用自身函数(Self-called);
2.计算仅占用常量栈空间(Stack Space)。
//Definition for a binary tree node.
function TreeNode(val) {
this.val = val;
this.left = this.right = null;
}
//二叉树求和
var sumNode = function(root) {
if(root===null) return 0;
return root.val + sumNode(root.left) + sumNode(root.right);
};