深刻理解--递归这个新概念

4 阅读6分钟

递归是一个让无数编程初学者懵逼的概念,偏偏它又是很多高级算法的基础。

递归本身的含义

递归,在计算机科学中是指一种通过 重复 将问题分解为 同类的子问题 而解决问题的方法。简单来说,递归表现为 函数 调用 函数本身。

在知乎看到一个比喻递归的例子,: 什么是递归?


递归最恰当的比喻,就是查词典。我们使用的词典,本身就是递归,为了解释一个词,需要使用更多的词。当你查一个词,发现这个词的解释中某个词仍然不懂,于是你开始查这第二个词,可惜,第二个词里仍然有不懂的词,于是查第三个词,这样查下去,直到有一个词的解释是你完全能看懂的,那么递归走到了尽头,然后你开始后退,逐个明白之前查过的每一个词,最终,你明白了最开始那个词的意思。


又例如 你浏览器搜索 “递归” 结果页面上又有一行字问你,您是不是要找‘递归’

image.png 点进去后,又会来到递归页面, 这仿佛是赛博鬼打墙,哈哈哈哈哈哈哈哈!

image.png

但递归真正的作用不是无线套娃下去 ,而是帮你解决复杂的问题。

mindmap
      更小问题
          小问题
            更小问题
            大问题
    
    

递归: 可以把一个大问题分解成更小的同类型问题 ,直到问题小到可以被直接解决,继而解决到原本的大问题

递归是需要一个终止条件的,当某个终止条件满足时,就不再递归调用啦! 要是你写出来的递归 没终止条件 亦或者说是一直到不了递归条件的话,会导致函数无限调用直到程序崩溃! 大家要注意这点!⚠️⚠️⚠️

那问题又来了,为什么程序无限调用会导致崩溃呢?「 栈溢出问题」「StackOverflowproblem」

  1. 因为每次调用函数,都会在内存新增一个栈帧,用来保存函数内部的状态,比如函数局部变量的值等等。
  2. 因为各个函数内部是互相独立的,但栈的空间是有限的,当递归调用的层级太多时,就会超出栈的容量,从而导致调用栈溢出。 ⚠️⚠️⚠️如果我们一味的调用函数而不返回,一旦栈被填满后,就会导致 栈溢出「StackOverflow」,程序会直接终止运行!
  3. 延伸一下,递归过程类似于出栈入栈。如果递归次数过多,栈的深度就需要越深,最后栈容量真的不够装。

递归的特点

实际上,递归有两个显著的特征,终止条件和自身调用:

• 自身调用:原问题可以分解为子问题,子问题和原问题的求解方法是一致的,即都是调用自身的同一个函数。

• 终止条件:递归必须有一个终止的条件,即不能无限循环地调用本身。

递归存在的问题

  • 递归调用层级太多,会导致栈溢出的问题。
  • 递归重复计算,会导致效率低下。

递归的种类

  • 在计算机科学和编程中,根据递归函数的结构和调用方式,可以将递归分为几种主要类型。

. 直接递归 (Direct Recursion)

这是最常见、最直观的递归形式。一个函数直接调用自己。

示例 :计算阶乘

image.png

. 间接递归 (Indirect Recursion)

在这种情况下,函数 A 调用函数 B,而函数 B 又反过来调用函数 A,形成一个调用循环。

示例:判断奇偶数

image.png 这段代码的目标是判断一个数字是偶数还是奇数

  • 如果是偶数,isEven(数字) 函数会告诉我们 true
  • 如果是奇数,isEven(数字) 函数会告诉我们 false

例如,isEven(2) 应该返回 trueisEven(3) 应该返回 false。 Qwen3-Coder


1. 直接递归 (Direct Recursion)

这是最常见、最直观的递归形式。一个函数直接调用自己

示例:计算阶乘

javascript

编辑

1function factorial(n) {
2    // 基准情况 (Base Case): 递归的终止条件
3    if (n <= 1) {
4        return 1;
5    }
6    // 递归步骤 (Recursive Step): 函数调用自身
7    else {
8        return n * factorial(n - 1);
9    }
10}
11
12// factorial(3) -> 3 * factorial(2) -> 3 * 2 * factorial(1) -> 3 * 2 * 1

2. 间接递归 (Indirect Recursion) 2. 间接递归(间接递归)

在这种情况下,函数 A 调用函数 B,而函数 B 又反过来调用函数 A,形成一个调用循环。

示例:判断奇偶数

javascript

编辑

1function isEven(n) {
2    if (n === 0) {
3        return true;
4    } else {
5        return isOdd(n - 1); // 调用 isOdd
6    }
7}
8
9function isOdd(n) {
10    if (n === 0) {
11        return false;
12    } else {
13        return isEven(n - 1); // 调用 isEven
14    }
15}
16
17// isEven(3) -> isOdd(2) -> isEven(1) -> isOdd(0) -> false

3. 尾递归 (Tail Recursion)

尾递归是一种特殊的直接递归。在函数的最后一步执行递归调用,并且该递归调用是函数返回的表达式,其返回值不需要再参与任何计算。

优点:尾递归在某些编程语言(如 Scheme、Scala)和编译器中可以被优化成循环,从而避免了创建新的栈帧,节省了内存,防止栈溢出。

示例:计算阶乘(非尾递归 vs 尾递归)

  • 非尾递归版本:

    javascript

    编辑

    1function factorial(n) {
    2    if (n <= 1) {
    3        return 1;
    4    }
    5    // 递归调用后还要进行乘法运算,所以不是尾递归
    6    return n * factorial(n - 1);
    7}
    
  • 尾递归版本:

    javascript

    编辑

    1function factorialTail(n, accumulator = 1) { // accumulator 用于累积结果
    2    if (n <= 1) {
    3        return accumulator;
    4    }
    5    // 递归调用是函数的最后一步,且结果直接返回,是尾递归
    6    return factorialTail(n - 1, accumulator * n);
    7}
    8
    9// factorialTail(3, 1) -> factorialTail(2, 3) -> factorialTail(1, 6) -> 6
    

4. 树形递归 (Tree Recursion)

当一个函数在其递归步骤中调用自身多次时,就形成了树形递归。这种递归的调用过程会形成一棵树状结构,通常会产生大量的重复计算。

用经典的二叉树 来理解树形递归吧

前序排列 (根左右)代码样式

image.png

中序排列(左根右)代码样式

image.png

后序排列(左右根)代码样式

image.png 解释一下: 前半部分就是将 二叉树用代码 表达出来 后半部分用代码将 二叉树 分别用三种方法 遍历出来

递归的经典应用场景

哪些问题我们可以考虑使用递归来解决呢?即递归的应用场景一般有哪些呢?

• 阶乘问题

• 二叉树深度

• 汉诺塔问题

• 斐波那契数列

• 快速排序、归并排序(分治算法也使用递归实现)

• 遍历文件,解析xml文件

厚积薄发 这只是其中的一小步 后续还有更多的知识需要我们去慢慢积累与学习!