Hi!这里是JustHappy!相信各位都有使用过递归吧?但是怎么说呢?似乎无论是学校的课本还是很多教程,都是直接丢给你这个概念,“对,这就是递归,就是在函数里调用函数本身”,但是?为啥呢?怎么用递归呢?我们在JavaScript中聊聊吧
何为递归?如何写一个递归?
我查了wiki百科的解释,但是感觉还是需要补充的,小弟斗胆解读一下,下面是我觉得重要的两句话
划重点!调用自身
字面意思的衍生“暴论” —— ”传递“ 与 ”回归“
我并没有查阅到较为权威的文献从字面意思去解读“递归”,但是我觉得“传递”和“回归”是递归这种解决方案的特点,
所谓传递不止有参数
通常我们写递归时,除了传递参数,我们其实也在传递“状态”。
举个例子,比如我们可以使用递归去遍历一个数组:
function sum(arr, index = 0) {
if (index === arr.length) return 0;
return arr[index] + sum(arr, index + 1);
}
我们通过“传递参数”实现了状态的传递:
回归
const arr = [1, [2, [3, 4]], 5];
function flatten(array) {
const result = [];
for (const item of array) {
if (Array.isArray(item)) {
result.push(...flatten(item)); // 递:深入处理嵌套数组
} else {
result.push(item); // 终止条件:遇到数字就收集
}
}
return result; // 回归:每一层把自己的结果“还”回去
}
console.log(flatten(arr)); // 输出: [1, 2, 3, 4, 5]
递归终止条件
终止条件是递归停止继续调用自身的时机,是整个递归的“出口”。
什么时候需要终止递归?
如果没递归终止条件这个出口,程序会爆栈(stack overflow) ,比如下面这个递归就永远不会停止:
function forever() {
return forever();
}
forever(); // RangeError: Maximum call stack size exceeded
如何写递归终止条件?
✅ 方式一:索引越界
function printArray(arr, i = 0) {
if (i >= arr.length) return;
console.log(arr[i]);
printArray(arr, i + 1);
}
✅ 方式二:元素类型满足某条件
function findFirstEven(arr, i = 0) {
if (i >= arr.length) return null;
if (arr[i] % 2 === 0) return arr[i];
return findFirstEven(arr, i + 1);
}
✅ 方式三:结构遍历到底
function printNested(obj) {
if (typeof obj !== 'object' || obj === null) {
console.log(obj);
return;
}
for (let key in obj) {
printNested(obj[key]);
}
}
printNested({ a: 1, b: { c: 2, d: { e: 3 } } });
“尾递归” 是个啥?
尾递归(Tail Recursion)指的是函数的最后一步操作是递归调用自身,并不依赖于调用的返回值进一步处理。
啊哈,这名字听起来有点技术含量(PS:不说人话),其实我们可以这样理解它:
尾递归 = 递归发生在函数的最后一步 (具体我们可以对比着普通递归说明)
普通递归(需要回溯,栈层层叠加):
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // 递归调用后的结果还要参与乘法
}
栈调用结构:
factorial(5)
=> 5 * factorial(4)
=> 5 * 4 * factorial(3)
=> ...
✅ 尾递归(直接返回,无需“回头处理”):
function factorialTR(n, acc = 1) {
if (n <= 1) return acc;
return factorialTR(n - 1, acc * n); // 递归调用是最后一步
}
尾递归结构:
factorialTR(5, 1)
=> factorialTR(4, 5)
=> factorialTR(3, 20)
=> ...
如果语言支持“尾调用优化”(TCO),尾递归就不会占用额外的栈空间,能避免爆栈。
但是......