背景
这次也没有老规矩,背景无,但是也还是要闲扯几句才进入主题。那就是说明这次文章的主题为啥是聊递归?主要还是之前写了一篇水文,现在来还账来了;还有就是最近复习,想重新深入了解一下啥子是递归?递归到底是个啥?嗯?感觉是同一个意思呢,那就换一个,递归到底怎么用,常见用到递归的场景有些啥?
好了,话不多说,请看下面浅见→
递归
什么是递归?
在程序中,递归就是函数自己直接或间接的调用自己。递归不能称得上是一种算法,而是应该理解为是一种符合人解题逻辑的编程技巧。比如数学中常见的问题:斐波那契数列、上楼梯、汉诺塔等问题,它们都能使用递归的思想那解决问题最终得到答案。
怎么理解递归?
要理解递归,那就必须知道什么是栈,毕竟递归需要利用栈这种数据结构来解决问题。那什么是栈呢?先看下面这张图,它就能很好表达栈的操作规则↓
栈是一种数据结构,是一种只能在一端进行插入和删除操作的特殊线性表。它按照先进后出的原则存储数据,先进入的被放入栈底(也称为进栈),后进入的数据放在栈顶,需要读数据的时候从栈顶开始弹出数据(弹出也可称为出栈或退栈)。
好了,概念了解了,那下面就要来理解递归了→
其实递归和普通函数调用没有什么区别,只是递归一般不能立即得到结果,而是需要经历挂起、入栈、出栈的过程才能解决问题。下面就用经典的斐波那契数列来演示递归的过程,如下:
斐波那契数列也称“兔子数列”,它是一个这样的数列:1,1,2,3,5,8,13,21,34,55,89...
// 斐波那契数列: 1 1 2 3 5 8 13 21 34 ...
// 从第三项开始,后面一项的值为前面两项相加之和
第一项: 1 ..............................1
第二项: 1 ..............................1
第三项: 1 + 1 ..........................2
第四项: 1 + 2 ..........................3
第五项: 2 + 3 ..........................5
第六项: 3 + 5 ..........................8
第n项: (n - 2) + (n - 1) ...............n
复制代码
转化为图,就是下图所示:
将上述分析转化为代码,如下:
function fibonacci(n) {
if (n <= 2) { // 终止条件
return 1;
}
return fibonacci(n - 2) + fibonacci(n - 1);
}
fibonacci(6); // 8
复制代码
执行上述代码就可以求得第N个斐波那契数值,光看代码和分析可能还不足够清晰,下面就分析第6项的值是如何求得的,如图:
分析:首先执行fibonacci(6)的时候会生成一个执行上下文,返回3 + fibonacci(5);接着需要调用fibonacci(5)再生成一个执行上下文,返回2 + fibonacci(4);接着需要调用fibonacci(4)再生成一个执行上下文,返回2 + fibonacci(3);接着需要调用fibonacci(3)再生成一个执行上下文,返回1 + fibonacci(2);接着执行fibonacci(2)和fibonacci(1);由于fibonacci(1)处于栈顶,不需要往下运行了,就开始弹出,返回结果1,接着弹出fibonacci(2),并返回结果1给执行上下文fibonacci(3);fibonacci(3)执行就是1 + 1 = 2,处于栈顶不需要执行了就把结果传递下去给fibonacci(4),fibonacci(4)执行就是1 + 2 = 3,同样往下,最后到达fibonacci(6)弹出,最后结果就为8。
这就是斐波那契数列的执行过程,因为前两项是固定的,所以将其设为终止条件,然后后面就依次类推计算第N项的值(注意:不是第N项的和!!!),最后得到第N项的值就等于前两项相加之和,即(n-2) + (n -1)。
递归的要素
看懂了上面的示例,那就可以从上面知道,递归需要满足的要素主要有下面两个条件:
- 自己调用自己
- 要有结束条件
这里就有问题要问,既然递归了解了,那它是为了解决怎样的问题引出来的?其实递归就是用来解决复杂问题的思想,比如说要你计算1~100的数字之和,你用for循环来一个一个往后循环,可是你用递归你就只需为其设置一个终止条件,让其自身调用自己就能获得最后结果了。如下:
// 将求100转换为求99
// 将求99转换为求98
// ...
// 将求2转换为求1
// 求1结果就是1
// 即:sum(1)是1
function sum(n) {
if (n == 1) {
return 1;
}
return sum(n - 1) + n;
}
var num = sum(100);
console.log(num); // 5050
复制代码
递归的练习
来,光说不练假把式,下面就用几个示例试试水,如下:
- 求阶乘!分析及代码如下:
- 1! .............1
- 2! .............1! * 2
- 3! .............2! * 3
- ...
- n! .............(n-1)! * n
function factorial(n) {
if ( n == 1) {
return 1;
}
return factorial(n - 1) * n;
}
// 5! = 1 * 2 * 3 * 4 * 5
console.log(factorial(5)); // 120
复制代码
- 求幂!分析及代码如下:
- 1 的 m 次方 1 * 1^(m - 1) = 1^m = 1
- 2 的 m 次方 m 个 2 相乘 2 * 2^(m - 1) = 2^m
- ...
- n 的 m 次方 m 个 n 相乘 即 n * n^(m - 1) = n^m
- 每个数的 1 次方都等于它本身,如2^1 = 2; 3^1 = 3 等
function power(n, m) {
if (m == 1){
return n;
}
return power(n, m-1) * n;
}
console.log(power(4,3)); // 4 * 4 * 4 = 48
复制代码
- 最后来个实际项目中经常遇到的,将平铺数组转为树,如下:
数据集合为:
const arr = [
{id: '1', parentId: 'root', name:'1'},
{id: '1-1', parentId: '1', name:'1-1'},
{id: '1-2', parentId: '1', name:'1-2'},
{id: '1-1-1', parentId: '1-1', name:'1-1-1'},
{id: '1-1-2', parentId: '1-1', name:'1-1-2'},
{id: '1-2-1', parentId: '1-2', name:'1-2-1'}
];
function getTreeData(data, pid) {
let copyData = JSON.parse(JSON.stringify(data));
let arr = [];
for (let i = 0; i < copyData.length; i++) {
if (copyData[i].parentId == pid) {
copyData[i].children = getTreeData(copyData, copyData[i].id);
arr.push(copyData[i]);
}
}
return arr;
}
const tree = getTreeData(arr, 'root');
console.log('tree', tree);
复制代码
至此,递归的相关知识就介绍完了,希望可以通过上面的示例帮助彼此更好的理解递归这个思想。实际项目中其实用到递归的思想也没多少,这就是经常说的,面试是面试,工作是工作。工作中会考虑算法的时间复杂度等,那这时递归的方式不是那么优秀,在性能方面也不是特别好,就拿上面举的将平铺数据转化为树结构数据,其实项目中就会选用其他方式来实现,只是这里聊的是递归,就用递归思想来演示一下。
作者:小黑豆和小土豆
链接:juejin.cn/post/709190…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。