• 怎么写递归


    以前我很少写递归,因为感觉写递归需要灵感,很难复制。看了《The Little Schemer》后,我发现写递归其实是有套路的。递归只需要想清楚 2 个问题:

    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. 阶乘、斐波那契

    阶乘、斐波那契用递归来写也是这个套路,代码结构都是一样的。

    先列出不需要计算的情况,再写大问题和小问题的转换关系。

    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. 小孩子的加法

    小孩子用数数的方式做加法,过程是这样的:

    3 颗糖 加 2 颗糖 是几颗糖?

    小孩子会把 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

    3. CPS 尾递归使用误区

    • cont 传入的参数不是最终结果。

      CPScont 是对结果的后续操作。
      也就是说 cont 传入的参数需要是最终结果。
      不满足这一点,就不能叫做 CPS

      错误代码示例:

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

      上述代码和之前代码的区别如下:

      x => cont(n) * x; // 现在 错误的写法
      x => cont(n * x); // 之前 正确的写法
      

      错误的写法中,cont 传入的参数不再是最终结果了,如果这么调用:

      factorial(5, console.log);
      

      期望控制台打印 120
      实际控制台打印 5

    • CPS,却不是尾递归。

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

      以上写法称得上是 CPS 了,但却不是尾递归。

      注意这段代码:

      cont(factorial(n - 1, x => n * x));
      

      factorial 的递归调用不是函数的最后一步,cont 的调用才是最后一步。


    4. 验证 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. 大问题怎么变成小问题
  • 相关阅读:
    《Linux就该这么学》第十二课
    《Linux就该这么学》第十一课
    《Linux就该这么学》第十课
    《Linux就该这么学》第九课
    《Linux就该这么学》第八课
    模板层
    路由层
    git的使用
    属性选择器
    高级选择器
  • 原文地址:https://www.cnblogs.com/apolis/p/11912823.html
Copyright © 2020-2023  润新知