用实战来理解递归(一)

397 阅读5分钟

1 什么是递归

  • 简单来说就是函数自己调用自己:
function f() {
  f();
}
f();

执行后发现 Maximum call stack size exceeded 爆栈了。

2 什么是一个正确的递归

  • 上面执行报错了,所以上面的说法不严谨,不爆栈的函数自己调用自己,才有可能是正确的递归

怎么做到不爆栈? 函数调用是一个不断往栈里面入栈和出栈的过程,所以保证入栈的数量即可,限制在一定范围内。

比如:

function f(x) {
  //参数x不断减少的情况,增加限制条件
  if(x == 0) return
  f(x-1);
}
f();


//or 

function f(x) {
 //参数x不断增加的情况,增加限制条件
  if(x > 10) return 
  f(x+1);
}
f();

从上面的例子我们可以知道,增加限制条件,不然函数继续执行就可以防止爆栈, 这个也叫增加递归的出口

现在可以理解为 有递归出口的函数自己调用自己就是递归 但是上面函数里面只增加了变量,没有什么意义,具体要在函数里面做点事儿。

3 用求n的阶乘来理解如何写递归

写递归的时候,要考虑三件事:

  1. 找重复: n * (n -1)的阶乘 (n-1)的阶乘 又可以分解为当n是n-1的时候 (n-1) * (n-1 -1 )的阶乘,可以进一步抽象成 n* f(n-1) f就是递归函数,是问题的重复,因为规模更小,所以这种重复叫子问题,想象成偷懒的感觉,我假设后面的结果都算好了,我只处理当前层的值 即可。
  2. 找变化:变化的量应该作为参数,参数是一个还是多个,什么时候用多个? 当前只有一个变量的情况无法继续求解的时候用,一个参数的情况,对一个参数进行增加或者减少的操作, 这里就是n每次减少1
  3. 找边界:就是上面提到的防止stackoverflow ,防止爆栈的情况,就是增加递归的结束条件,即递归出口

所以经过上面的分析, 写一个递归的大致框架是:

//1 定义函数 n是找到的变化
//这里的难点是,我怎么知道它怎么求的? 我们定义函数f(n) 是求n的阶乘   fn(n-1)就是求 f(n-1)的阶乘,不要管怎么求的,你怎么定义的它就有这样的功能。
function f(n) {
  //3  定义递归出口 防止爆栈
  if (n == 1) return 1;
  //2 寻找子问题 递归求解
  return n * f(n - 1);
}

let n = 5;
console.log(f(n)); //120

我们再看一个实际的例子:求i到j的直接的和(i<j)

4 如何将循环转化成递归的写法?

求i到j的直接的和(i<j)

循环写法:

const sum = (i, j, res = 0) => {
  for (let k = i; k <= j; k++) {
    res += k;
  }
  return res;
};
console.log(sum(1, 100));  //55

改成递归: 用上面递归解法的大致框架思路为:

1 先看是否可以找到重复? 可以 当前值加后面求和的值 2 再看找找变化 每次变量+1 3 找出口 i>j的时候 停止递归

//找重复是 当前sumRecursive 计算当前值 后面的值可以让 sumRecursive(i+1) 决定
//找变化是 i+1是变化 
//找递归出口是 if(i>j ) return 0 是递归出口 要返回一个数字还是0 不然累加会影响结果,不是数字会返回NaN 因为undefined + 数字 变成NaN
const sumRecursive = (i, j) => {
  if (i > j) return 0;
  return i + sumRecursive(i + 1, j);
};

console.log(sumRecursive(1, 10));//55


//i<j 的情况 j递减实现也是可以的
const sumRecursive = (i, j) => {
  if (j < i) return 0;
  return j + sumRecursive(i, j - 1);
};

console.log(sumRecursive(1, 10)); //55

5 数组求和

let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

//数组高阶函数 迭代实现
const sum = arr => arr.reduce((cur, next) => cur + next, 0);
// console.log(sum(arr)); //55

//递归实现
const sumRecursive = (arr, i = 0) => {
  //1找重复 求和
  //2找变化 下标的变化 所以要想到加参数,不加参数实现不了
  //3递归出口
  // if (i == arr.length) return 0;
  if (i == arr.length - 1) return arr[i];
  //重复就是当前项的值 + 其他项的值(递归实现)
  return arr[i] + sumRecursive(arr, i + 1);
};

console.log(sumRecursive(arr)); //55

6 翻转字符串

  • 思路 下标从字符串长度开始逐渐减少, 到零为止 ,为什么不能是增加? 因为要翻转字符串,这就是变化和为零递归出口, 重复也就是子问题就是每次求得当前下标对应的字符 再跟其他相加拼接即可。
let str = "hello";

//思路 下标从字符串长度逐渐减少, 到零为止 ,为什么不能是增加? 因为要翻转字符串
const reverseStr = (str, i = str.length) => {
  //变化的是下标, 下标可以从末尾变化 , 所以这里要加参数
  //出口是下标小于0的情况 不再处理
  //重复就是当前项 + 递归子问题的下一项
  if (i == 0) {
    return str.charAt(i) + "";
  }
  return str.charAt(i) + reverseStr(str, i - 1);
};

console.log(reverseStr(str)); //olleh

不用charAt实现:

let str = "hello";

const reverseStr = (str, i = str.length - 1) => {
  if (i == 0) return str[i];
  return str[i] + reverseStr(str, i - 1);
};
console.log(reverseStr(str)); //olleh

7 斐波那契数列

//递归
const fib = n => {
  console.count();
  if (n <= 1) return 1;
  return fib(n - 1) + fib(n - 2);
};
// 1 1 2 3 5 8 13
//子问题的分解, 可以分为 当前值 + 小规模子问题
//            也可以分为 多个小规模子问题  比如fib
// n = 1 || n =2  2
// n>2 f(n) = f(n-1) + f(n-2)
fib(5);

//recursive with cache //带缓存的递归
const fibonacci = (n, memo = {}) => {
  console.count();
  //has cache return it
  if (memo[n]) return memo[n];
  if (n <= 1) return 1;
  return (memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo));
};

// fibonacci(8);
//迭代
function fibonacciIter(num) {
  var a = 1,
    b = 0,
    temp;

  while (num >= 0) {
    temp = a;
    a = a + b;
    b = temp;
    num--;
  }

  return b;
}

fibonacciIter(5);

//通项公式
let fib09 = n =>
  Math.round(
    (Math.pow((1 + Math.sqrt(5)) / 2, n) -
      Math.pow((1 - Math.sqrt(5)) / 2, n)) /
      Math.sqrt(5)
  );

//js严格模式下尾递归
("use strict");
function fibonacci10(n, n1, n2) {
  if (n <= 1) {
    return n2;
  }
  return fibonacci(n - 1, n2, n1 + n2);
}
//一定范围内查表法
const fib11 = n => {
  let table = [
    0,
    1,
    1,
    2,
    3,
    5,
    8,
    13,
    21,
    34,
    55,
    89,
    144,
    233,
    377,
    610,
    987,
    1597,
    2584,
    4181,
    6765,
    10946,
    17711,
    28657,
    46368,
    75025,
    121393,
    196418,
    317811,
    514229,
    832040
  ];
  return table[n] || "当前数据太大 不再查找的表内!";
};
//矩阵乘法 太复杂 不写了

8 最大公约数

辗转相除法

//子问题 f(m,n) = f(n , m%n)
//递归出口 n= 0
const gdc = (m, n) => {
  if (n == 0) return n;
  return gdc(n, m % n);
};

9 汉诺塔


const hano = (n, from, to, help) => {
  if (n == 1) {
    console.log(`move ${n} from ${from} to ${to}`);
    return;
  } else {
    hano(n - 1, from, help, to); //先把前n-1个盘到辅助盘
    console.log(`move ${n} from ${from} to ${to}`);
    hano(n - 1, help, to, from); //让n-1 从辅助盘 到目标盘 借助源盘
  }
};

hano(3, "A", "B", "C");
//7步
// move 1 from A to B
// move 2 from A to C
// move 1 from B to C
// move 3 from A to B
// move 1 from C to A
// move 2 from C to B
// move 1 from A to B