寫寫我理解的遞歸

遞歸

階乘

先用一個例子說明:

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,你就必須讓nn-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。

  1. 想要移動n個盤子,需要把上面的n-1個盤子移動到輔助柱子上,然後移動最底下的盤子到目標,最後,把輔助柱子上的盤子移動到目標柱子。
  2. 如果遞歸想不通,那麼思考下循環,誰是第一個被移動的盤子(假設最底下是3號盤子)?

    • 想移動3號盤子,需要移動2號盤子,想移動2號,盤子,需要移動1號盤子。所以在遞歸中,1號盤子先被移動。

    • 1號盤子先被移動到那個柱子上呢?玩遊戲我發現,單數盤子想要移動到目標柱子上,需要最頂層的盤子(1號盤子)先移動到目標柱子上;如果是雙數盤子,則需要最頂層的盤子先移動到輔助柱子上。所以,到底1號盤子被先移動到那個柱子上,和盤子數有關。

    • 從算法中我們亦可以看到,hanoi(disc-1 , src , dst , mid);交換了目標柱子和輔助柱子,如果disc爲偶數,則1號(最頂層的)移動到了中間柱子(mid);如果disc爲奇數,則1號盤子移動到了目標柱子(dst)。

  3. 畫出移動盤子的樹形圖

這種訪問順序讓我想起了中序遍歷,但是和中序遍歷有區別,它的每一層節點都是同一個節點。

中序遍歷

 //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的值,而是:

  1. 不停地訪問他的左節點,直到null爲止,然後獲取最近的父節點的值。
  2. 然後再按照步驟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);
};
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章