尾递归的理解与应用

通过示例理解尾递归的优化原理,以及在数组求和、扁平化等场景的应用

问题

什么是尾递归?它与普通递归有什么区别?有哪些实际应用场景?

解答

递归基础

递归是指在函数定义中调用函数自身的方法。它将复杂问题转化为规模较小的相似问题来求解。

以计算 x 的 n 次方为例,普通递归实现:

function pow(x, n) {
  if (n == 1) {
    return x;
  } else {
    return x * pow(x, n - 1);
  }
}

计算 pow(2, 4) 的执行过程:

pow(2, 4) = 2 * pow(2, 3)
pow(2, 3) = 2 * pow(2, 2)
pow(2, 2) = 2 * pow(2, 1)
pow(2, 1) = 2

每次调用都需要保存上一层的状态,递归次数过多会造成栈溢出。

尾递归优化

尾递归是在函数尾部直接调用自身的递归形式。由于只存在一个调用记录,不会发生栈溢出。

对比普通递归和尾递归实现阶乘:

普通递归(复杂度 O(n)):

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120

尾递归(复杂度 O(1)):

function factorial(n, total = 1) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5) // 120

尾递归每次返回的是新函数调用,不携带上一个函数的参数,因此只需保存一个调用栈。

应用场景

数组求和:

function sumArray(arr, total = 0) {
  if (arr.length === 0) {
    return total;
  }
  return sumArray(arr.slice(1), total + arr[0]);
}

斐波那契数列:

function fibonacci(n, start = 1, total = 1) {
  if (n <= 2) {
    return total;
  }
  return fibonacci(n - 1, total, total + start);
}

数组扁平化:

function flat(arr = [], result = []) {
  arr.forEach(v => {
    if (Array.isArray(v)) {
      result = result.concat(flat(v, []));
    } else {
      result.push(v);
    }
  });
  return result;
}

let a = [1, 2, 3, [1, 2, 3, [1, 2, 3]]];
flat(a); // [1, 2, 3, 1, 2, 3, 1, 2, 3]

关键点

  • 尾递归是在函数尾部调用自身,每次返回新函数调用而不保存上层状态
  • 尾递归只需保存一个调用栈,空间复杂度为 O(1),避免栈溢出
  • 通过累加器参数(如 total)将中间结果传递给下一次调用
  • 适用于数组求和、斐波那契数列、数组扁平化等递归场景
  • JavaScript 引擎需要支持尾调用优化(TCO)才能真正实现优化效果