遞歸
階乘
先用一個例子說明:
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);
};