怎麼寫遞歸

之前我不多寫遞歸,由於感受寫遞歸須要靈感,很難複製。javascript

學了點函數式後,我發現寫遞歸實際上是有套路的。
遞歸只須要想清楚 2 個問題:java

  1. 什麼狀況不須要計算
  2. 大問題怎麼變成小問題

舉例

1. 判斷數組是否包含某元素

const has = (element, arr) => {};
  • 什麼狀況不須要計算?
    數組爲空時不須要計算,必定不包含。數組

    const has = (element, arr) => {
      if (arr.length === 0) return false;
    };
  • 怎麼把大問題變成小問題?
    arr 的長度減少,向數組爲空的狀況逼近。
    arr 中取出第一個元素和 element 比較:瀏覽器

    1. 相同:返回 true
    2. 不相同:求解更小的問題。
    const has = (element, arr) => {
      if (arr.length === 0) return false;
      else if (arr[0] === element) return true;
      else return has(element, arr.slice(1));
    };

2. 刪除數組的某個元素

const del = (element, arr) => {};
  • 什麼狀況不須要計算?
    數組爲空時不須要計算,返回空數組。函數

    const del = (element, arr) => {
      if (arr.length === 0) return [];
    };
  • 怎麼把大問題變成小問題?
    arr 的長度減少,向空數組的狀況逼近。
    arr 中取出第一個元素和 element 比較:優化

    1. 相同:返回數組餘下元素。
    2. 不相同:留下該元素,再求解更小的問題。
    const del = (element, arr) => {
      if (arr.length === 0) return [];
      else if (arr[0] === element)
        return arr.slice(1);
      else
        return [
          arr[0],
          ...del(element, arr.slice(1))
        ];
    };

3. 階乘、斐波那契

階乘、斐波那契用遞歸來寫也是這個套路,代碼結構都是同樣的。code

先列出不須要計算的狀況,再寫大問題和小問題的轉換關係。遞歸

const factorial = n => {
  if (n === 1) return 1;
  else return n * factorial(n - 1);
};
const fibonacci = n => {
  if (n === 1) return 1;
  else if (n === 2) return 1;
  else return fibonacci(n - 1) + fibonacci(n - 2);
};

4. 小孩子的加法

小孩子用數數的方式作加法,過程是這樣的:ip

3 顆糖 加 2 顆糖 是幾顆糖?ci

小孩子會把 3 顆糖放左邊,2 顆糖放右邊。
從右邊拿 1 顆糖到左邊,數 4,
再從右邊拿 1 顆糖到左邊,數 5,
這時候右邊沒了,得出有 5 顆糖。

這也是遞歸的思路。

const add = (m, n) => {};
  • n = 0 時,不須要計算,結果就是 m

    const add = (m, n) => {
      if (n === 0) return m;
    };
  • 把問題向 n = 0 逼近:

    const add = (m, n) => {
      if (n === 0) return m;
      else return add(m + 1, n - 1);
    };

固然 m = 0 也是不須要計算的狀況。
選擇 m = 0 仍是 n = 0 做爲不須要計算的狀況 決定了 大問題轉成小問題的方向。


Continuation Passing Style

const add1 = m => m + 1;

add1 的返回結果乘 2,一般這麼寫:

add1(5) * 2;

Continuation Passing Style 來實現是這樣的:

const add1 = (m, continuation) =>
  continuation(m + 1);

add1(5, x => x * 2);

add1 加一個參數 continuation,它是一個函數,表示對結果的後續操做。


咱們用 Continuation Passing Style 來寫寫遞歸。

如下用

  1. CPS 代替 Continuation Passing Style
  2. cont 代替 continuation

1. 階乘

const factorial = (n, cont) => {
  if (n === 1) return cont(1);
  else return factorial(n - 1, x => cont(n * x));
};
  • 若是 n === 1,把結果 1 交給 cont
  • 若是 n > 1,計算 n - 1 的階乘,
    n - 1 階乘的結果 xn,交給 cont

這個 factorial 函數該怎麼調用呢?
cont 能夠傳 x => x,這個函數接收什麼就返回什麼。

factorial(5, x => x);

  • 以前的寫法:

    const factorial = n => {
      if (n === 1) return 1;
      else return n * factorial(n - 1);
    };

    遞歸調用 factorial 不是函數的最後一步,還須要乘 n
    所以編譯器必須保留堆棧。

  • 新寫法:

    const factorial = (n, cont) => {
      if (n === 1) return cont(1);
      else
        return factorial(n - 1, x => cont(n * x));
    };

    遞歸調用 factorial 是函數的最後一步。
    作了尾遞歸優化的編譯器將不保留堆棧,從而不怕堆棧深度的限制。

也就是說:能夠經過 CPS 把遞歸變成尾遞歸。


2. 斐波那契

const fibonacci = (n, cont) => {
  if (n === 1) return cont(1);
  else if (n === 2) return cont(1);
  else
    return fibonacci(n - 1, x =>
      fibonacci(n - 2, y => cont(x + y))
    );
};
  • 若是 n === 1,把結果 1 交給 cont
  • 若是 n === 2,把結果 1 交給 cont
  • 若是 n > 2
    計算 n - 1 的結果 x
    計算 n - 2 的結果 y
    x + y 交給 cont

CPS 能夠把遞歸變成尾遞歸,但並非用了 CPS 的遞歸就是尾遞歸。

像這麼寫,就不是尾遞歸:

const fibonacci = (n, cont) => {
  if (n === 1) return cont(1);
  else if (n === 2) return cont(1);
  else
    return fibonacci(n - 1, x =>
      cont(fibonacci(n - 2, y => x + y))
    );
};

注意這段代碼:

x => cont(fibonacci(n - 2, y => x + y));

fibonacci 的調用不是函數的最後一步,cont 的調用纔是最後一步。


3. 驗證 CPS 尾遞歸優化

截止到 2019 年 11 月,只有 Safari 瀏覽器宣稱支持尾遞歸優化。

用從 1 加到 N 的例子試驗了一下,Safari 13.0.3:

  • 通常遞歸
    報錯:堆棧溢出

    "use strict";
    
    const sum = n => {
      if (n === 1) return 1;
      else return n + sum(n - 1);
    };
    
    sum(100000);
  • CPS 尾遞歸
    正常算出結果

    "use strict";
    
    const sum = (n, cont) => {
      if (n === 1) return cont(1);
      else return sum(n - 1, x => cont(n + x));
    };
    
    sum(1000000, x => x);

最後想說的

用之前的方式寫遞歸 仍是 用 CPS 寫遞歸,只是寫法上不一樣,思想都是同樣的,都是要搞清:

  1. 什麼狀況不須要計算
  2. 大問題怎麼變成小問題
相關文章
相關標籤/搜索