一文搞懂“尾递归”

1,559 阅读3分钟

面试中经常被问到一些名词,见过用过也貌似能说个大概,但是感觉就是理解不到位,回答地没底气。

我打算做一期专栏,帮助同学们夯实基础知识,提升程序员的内在修养,让你在面试或者平时工作中,能做到信手拈来,对答如流。本篇文章我们的主体是“尾递归”。

什么是尾递归

尾递归是指一个函数在调用自身之后不再执行任何其他操作,而是将返回值直接传递给函数调用的上级,从而避免了新的调用栈帧的创建。换句话说,尾递归是指递归调用发生在函数的最后一个语句中,从而使得函数调用不需要保存多个调用栈帧,而只需一个即可。

下面是一个简单的递归函数的示例,使用递归计算斐波那契数列的第 n 项:

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

上面的递归函数在计算较大的斐波那契数列时,会因为递归调用太多而导致栈溢出的问题。为了避免这个问题,我们可以使用尾递归优化:

function fibonacci(n, prev = 0, next = 1) {
    if (n === 0) { return prev; }
    return fibonacci(n - 1, next, prev + next);
}

上面的函数使用了尾递归优化,可以避免递归调用带来的内存占用和栈溢出的问题。这个函数的实现方式是使用两个变量 prev 和 next,表示当前计算的斐波那契数列的前两项,然后使用递归方式,不断更新 prev 和 next 的值,直到计算到第 n 项。

下面是一个使用尾递归实现的数组求和函数的示例:

function sum(arr, total = 0) {
    if (arr.length === 0) { return total; }
    const [head, ...tail] = arr;
    return sum(tail, total + head);
}

上面的函数使用尾递归的方式来实现数组求和,避免了递归调用带来的内存占用和栈溢出的问题。

在函数式编程中,尾递归通常是使用高阶函数来实现的。下面是一个使用高阶函数实现的递归函数的示例:

function factorial(n) {
    function iter(n, acc) {
        if (n === 0) { return acc; }
        return iter(n - 1, acc * n);
    }
    return iter(n, 1);
}

上面的函数使用了高阶函数 iter,通过递归调用 iter 函数来实现阶乘的计算。iter 函数使用了两个参数,n 表示当前要计算的数,acc 表示当前的累积结果。在每次递归调用时,更新 n 和 acc 的值,直到 n 等于 0,然后返回 acc 的值作为最终的结果。这个函数的实现方式也使用了尾递归的优化,避免了递归调用带来的内存占用和栈溢出的问题。

尾递归的特点是能够优化递归算法的性能,因为它避免了不必要的调用栈帧的创建,并使得递归算法的空间复杂度降低到 O(1)。尾递归在一些函数式编程语言(如Scheme)中是很重要的概念,因为它允许使用递归来实现循环,而不会导致栈溢出等问题。

尾递归跟常规的递归有哪些区别

常规递归和尾递归的区别在于函数的调用和返回顺序以及对栈空间的利用方式不同。

具体来说,常规递归的过程是:每次函数调用时需要记录当前函数上下文的信息,包括传入参数、局部变量值等,并将这些信息保存在栈空间(也称调用栈或运行时栈)中。在递归过程中,每一次递归调用都会新建一个对应的栈帧(栈的一层)并保存上述信息。

而尾递归则是指递归过程中,只有一个栈帧被不断的更新。即,函数执行完成后直接返回结果,不再进行任何其他操作。这样一来,由于只占用一个栈帧,程序在空间上会得到很大优化,极大地降低了内存负担。同时,尾递归也可以被优化为迭代的形式,进一步减少时间负担。

总的来说,尾递归在递归过程中最后一步执行递归函数的步骤,并且在函数结束前不再需要执行任何其他计算或操作;而常规递归则在递归嵌套过程中创建多个栈帧以保存函数调用的相关信息。

尾递归的使用场景

尾递归由于其优化了空间复杂度和执行速度的特性,应用场景如下:

  1. 需要进行深度遍历的算法,例如深度优先搜索(DFS)和回溯等。在这些算法中,尾递归可以避免递归深度过大,导致栈溢出的问题。
  2. 函数可以以迭代的形式实现的递归算法,例如阶乘、斐波那契数列、十进制转二进制等算法。此时,使用尾递归不仅可以避免递归深度过大,而且可以避免中间过程的计算重复。
  3. 在一些函数式编程语言中,尾递归常常被用来优化递归调用的性能。

需要注意的是,并非所有递归算法都适合使用尾递归。对于递归算法来说,当递归调用发生在函数执行过程中,而不是在函数返回后的最后一步时,很可能会导致栈空间的浪费和时间复杂度的问题,此时不能使用尾递归进行优化。

在前端领域,尾递归有哪些技术实践

尾递归技术在前端开发领域的应用比较广泛,下面给出一些常用的框架或库的示例:

  • Lodash 库

Lodash 是一个非常流行的 JavaScript 工具库,提供了很多常用的函数和工具函数。其中就包括了一些使用尾递归技术实现的函数。比如,Lodash 的 flattenDeep 函数可以使用尾递归实现:

function flattenDeep(array) {
    return array.reduce((acc, value) => {
        return Array.isArray(value) ? acc.concat(flattenDeep(value)) : acc.concat(value);
    }, []);
}

上面的代码使用 reduce 函数和尾递归的方式来实现 flattenDeep 函数,避免了递归调用带来的内存占用和栈溢出的问题。

  • Ramda 库

Ramda 是一个函数式编程库,提供了很多函数式编程的工具函数。其中也包括了一些使用尾递归技术实现的函数。比如,Ramda 的 reduce 函数可以使用尾递归实现:

function reduce(fn, acc, list) {
    if (list.length === 0) { return acc; }
    const [head, ...tail] = list;
    const newAcc = fn(acc, head);
    return reduce(fn, newAcc, tail);
}

上面的代码使用递归和尾递归的方式来实现 reduce 函数,避免了递归调用带来的内存占用和栈溢出的问题。

  • RxJS 库

RxJS 是一个响应式编程库,提供了很多响应式编程的工具函数。其中也包括了一些使用尾递归技术实现的函数。比如,RxJS 的 scan 函数可以使用尾递归实现:

function scan(accumulator, seed) {
    return function (source) {
        return new Observable(function (observer) {
            let acc = seed;
            return source.subscribe({
                next(value) {
                    acc = accumulator(acc, value);
                    observer.next(acc);
                }, error(err) {
                    observer.error(err);
                }, complete() {
                    observer.complete();
                }
            });
        });
    }
}

上面的代码使用递归和尾递归的方式来实现 scan 函数,避免了递归调用带来的内存占用和栈溢出的问题。

这些示例都使用了尾递归技术,提高了代码的性能和可维护性,避免了递归调用带来的内存占用和栈溢出的问题。 在使用尾递归的过程中需要什么注意事项

在试用尾递归的过程中,需要注意以下几点:

  1. 一定要确保递归的结束条件是正确的。否则递归可能会进入无限循环,导致栈溢出错误。
  2. 在递归函数中将参数处理后传递给下一次递归时,需要确保传递的参数是符合要求的。否则递归算法可能会失去原本的意义,导致结果不正确。
  3. 尾递归需要广泛支持,否则递归可能会被执行多次,而不是直接被编译器优化。例如,一些 JavaScript 引擎可能不支持尾递归优化。
  4. 尽可能使用尾递归来代替常规的递归,以减小内存占用并提高程序的效率。但也要注意,不是所有的递归算法都适合使用尾递归,需要具体情况具体分析。
  5. 在使用尾递归的过程中,的确能够减少内存消耗,但也要注意不要因此忽视代码的可读性和可维护性。在编写尾递归函数时,需要注意注释,变量名,代码风格等其他代码质量方面的问题,以便于维护和协作。

总的来说,尾递归能够优化递归算法的性能和空间复杂度,但在将递归算法转化为尾递归时,需要注意细节和实现方法,以确保代码的正确性、稳定性和可读性。