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