1. 递归
1.1 理解递归
递归是一种解决问题的方法,它从解决问题的各个小部分中开始,直到解决最初的大问题,递归通常涉及到函数自身调用自身.
递归从作为一种算法在程序设计语言中广泛应用,通常它是将一个大型的复杂问题层层转换为一个与原问题相似的规模较小的问题来求解,合理利用递归可以极大地减少代码量.
- 递归函数
递归函数通常像下面这种能够直接调自身方法或函数.
function recursionFuntion(params){
recursiverFunction(params)
}
能够像下面能间接调用自身函数的,也是递归函数
function recursionFuntion(params){
middleFn(params)
}
function middle(params){
recursionFuntion(params)
}
假设我们执行上面的递归函数,结果会是它会一直执行,因此, 一般来说递归函数通常要有基线条件,即一个不在递归调用的条件(停止点). 以防止无限递归.
在简单理解了递归之后, 我们利用递归来模拟一个非常简单的例子
function Recursion(){
const resursionAnswer = confirm('你是否理解递归');
if(resursionAnswer){
return true
}
return Recursion()
}
上面是一个非常简单的递归案列, Recurtion函数会不断调用自身,直到resursionAnswer为true, 即当用户点击了确定时停止并返回对应的true. 上面的resursionAnswer为真就是上述代码的基线条件.
1.2 递归的简单案列
1.2.1计算一个数的阶乘
计算阶乘是作为递归最典型的一个例子, 首先我们来简单介绍一下什么是阶乘,阶乘是数学中的概念,定义为: 数n的阶乘, 定义为n!, 表示从1到n的整数的乘积. 例如5的阶乘表示!5, 即等于`5*4*3*2*1` 它们的乘积之和.结果为120.
利用迭代求阶乘
function factorial(number) {
if (number < 0) return undefined;
let total = 1;
let num = number;
while(num > 1){
total *= num
num--
}
return total
}
我们利用给定的number开始计算阶乘,每次依次减一, 并依次相乘, 通过total变量进行保存对应的乘积, 直到变量num为1时停止.0的阶乘也是1, 负数的阶乘不会被计算.
递归阶乘
5的阶乘可以用**5*!4**来计算, 4的阶乘可以用**4*!3**来进行计算,4的阶乘可以用**4*!3**来进行计算,计算3的阶用**3*!2**来进行计算.依次类推, 直到计算1的阶乘停止,我们可以得出计算n-1的阶乘就是就是n的阶乘的原始问题的一个子问题, 所以我们可以利用递归写出如下代码.
function factorial(num){
// 基线条件
if(num === 1 || num === 0) return 1;
// 递归调用
return num * factorial(--num)
}
值得注意的是, 当我们如果忘记加上用于停止函数递归调用的基线条件,递归并不会无限的执行下去,浏览器会抛出错误,也就是所谓的栈溢出错误,.在谷歌浏览器中会抛出`Uncaught RangeError: Maximum call stack size exceeded`的错误.
1.2.1 斐波那契数列
斐波那契数量也是一个可以递归的问题,它是一个由0,1,1,2,3,5,8,13,21,34...以此类推组成的序列.我们可以很明显地看到,数2由1+1得到,数3由1+2得到,数5由2+3得到,以此类推, 斐波那契数列的定义如下.
- 位置0的斐波那契数是0.
- 1和2的斐波那契数是1.
- n(此处 n > 2) 的斐波那契数是(n - 1) 的斐波那契数+(n - 2)的斐波那契数.
迭达求斐波那契数
function fibonacci(n) {
if (n === 0) return 0;
if (n <= 2) return 1;
let fibonaccis1 = 1;//n-1位初始的斐波那契数
let fibonaccis2 = 0;// n-2位初始的斐波那契数
let fiboNumber = n;
for (let i = 2; i < n; i++) { // n >= 2
fiboNumber = fibonaccis1 + fibonaccis2;// f(n- 1) + f( n + 2)
fibonaccis2 = fibonaccis1;
fibonaccis1 = fiboNumber
}
return fiboNumber
}
上面是通过迭代方法实现的求斐波那契数的简单方法,当传入的n等于0 时返回0, 当传入的n <= 2时则返回1,当上面的条件都不成立即n >2 时进行迭达, 通过fibonaccis1来保存n-1为的斐波那契数,fibonaccis2保存n-2位的斐波那契数,然后fiboNumber用来保存最终返回的斐波那契数, 每当fiboNumber进行一次赋值,则此时n-1处和n-2的斐波那契数都得发生改变,很显然n-1处的斐波那契数应该等于进行赋值的新fiboNumber, n-2处即为n-1之前的斐波那契数.当迭代结束则返回对应斐波那契数.
我们可以看到利用迭代求斐波那契数比较麻烦, 代码实现起来比较多.接下来我们利用递归来简单实现一下.
function fibonacciMemo(n) {
const meno = [0, 1];
if(n < 1 ) return 0
if(n <= 2) return 1
const fibonacci = n => {
if (meno[n-1] !== undefined ) return meno[n-1];
return meno[n-1] = fibonacci(n - 1) + fibonacci(n - 2)
}
return fibonacci(n)
}
在上面的代码中,我们通过meno数组来缓存所有的计算结果,如果已经被计算过了,我们就直接返回它,否则计算该结果,并将它添加到缓存中.用于数组中的位置是从o开始计算的,而我们传入的n默认是从1开始计数的, 所有我们对于的在数组储存中的位置应该减一.
1.2.3递归打印文件
假设当前js文件有如下目录, 它的目录如下, 此时我们想在控制台中打印当前递归访问文件.js
所在目录下的所有文件,此时我们利用递归就非常简单.
下面是简单的代码实现
const fs = require('fs');
const path = require('path');
// 读取当前目录下的所有文件 dirList为文件列表,dirname表示当前的绝对路径
function readdir(dirList,dirname){
function readerFile(filename){
// 返回一个包含文件信息的描述对象
let stat = fs.lstatSync(path.join(dirname,filename));
// 如果当前文件类型为文件夹则递归读取, 并传入当前的新路径
if(stat.isDirectory()){
let res = fs.readdirSync(path.join(dirname,filename));
return readdir(res,path.join(dirname,filename))
}
// 读取文件
fs.readFile(path.join(dirname,filename),(err,files) =>{
if(err) return err
// 利用toString方法打印文件中的内容
console.log(files.toString())
})
}
// 遍历文件列表, 并解析
dirList.map(filename => readerFile(filename))
}
//利用readdirSync同步读取当前目录下的所有文件的文件名,并传入当前的文件的绝对路径
readdir(fs.readdirSync(__dirname),__dirname);
上面的代码是使用了node中的fs模块来解析文件, 利用path模块来解析路径, 此时当我们在控制台输入
node 递归访问文件.js, 会在控制台依次打印出每个文件中的具体内容.
1.3 为什么使用递归?
在很多场景下, 使用递归可以极大的简化我们的代码,使我们的代码更易于理解,但是通常场景下, 使用迭代的版本比使用的递归的版本的运行效率更快.在一些算法来说, 使用迭代的解法可能不可用,而且当递归使用了尾调用优化.递归产生的多余消耗甚至可能被消除.所以我们经常使用递归来简化问题的复杂性,因为它解决问题会非常简单.
网友评论