怎么写递归

之前我不多写递归,由于感受写递归须要灵感,很难复制。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. 大问题怎么变成小问题
相关文章
相关标签/搜索