递归算法入门:像俄罗斯套娃一样思考

用代码拆解"函数调用自己"的奇妙思维

你有没有拆过俄罗斯套娃?打开一个,里面还有一个,再打开,又一个......直到最小的那个。递归(Recursion)就是这种"自相似"的思维方式------函数在执行过程中调用自身,像一面镜子照出另一面镜子。

本文会带你从零理解递归,并附上可直接运行的代码示例,让你真正掌握这个编程核心技巧。


什么是递归?

递归 = 递推 + 回归

  • 递推:把大问题分解成形式相同但规模更小的子问题

  • 回归:子问题解决后,逐层返回结果,拼出原问题的答案

一句话:把大象装进冰箱需要几步? 递归的回答是:1. 先放进去一只小象;2. 剩下的事和之前一样。


递归的两个关键要素

  1. 基线条件(Base Case) :问题小到可以直接解决,不再调用自身。没有它,递归会无限循环(就像没有底的套娃)。

  2. 递归条件(Recursive Case):把问题拆小,并调用自身去处理子问题。

💡 经验法则:写递归前,先想清楚"最简单的情况是什么",再想"如何把复杂情况变简单"。


经典案例:阶乘(n!)

阶乘的定义:

  • 0! = 1

  • n! = n × (n-1)!

这本身就是递归定义。

JavaScript 实现

javascript

复制代码
function factorial(n) {
  // 基线条件
  if (n === 0 || n === 1) {
    return 1;
  }
  // 递归条件
  return n * factorial(n - 1);
}

console.log(factorial(5)); // 120
// 执行过程:
// 5 * factorial(4)
// 5 * 4 * factorial(3)
// 5 * 4 * 3 * factorial(2)
// 5 * 4 * 3 * 2 * factorial(1)
// 5 * 4 * 3 * 2 * 1 = 120

Python 实现

python

复制代码
def factorial(n):
    if n <= 1:
        return 1
    return n * factorial(n - 1)

print(factorial(5))  # 120

另一个经典:斐波那契数列

数列:0, 1, 1, 2, 3, 5, 8, 13......

规律:F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2)

javascript

复制代码
function fibonacci(n) {
  if (n === 0) return 0;
  if (n === 1) return 1;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// 输出前10项
for (let i = 0; i < 10; i++) {
  console.log(`F(${i}) = ${fibonacci(i)}`);
}

⚠️ 这种写法简单易懂,但效率极低(指数级重复计算)。实际开发中可用记忆化递归循环优化。


递归 vs 循环:一个直观对比

任务:计算 1 到 n 的和

循环(迭代)方式

javascript

复制代码
function sumLoop(n) {
  let total = 0;
  for (let i = 1; i <= n; i++) {
    total += i;
  }
  return total;
}

递归方式

javascript

复制代码
function sumRecur(n) {
  if (n === 1) return 1;
  return n + sumRecur(n - 1);
}
维度 递归 循环
代码简洁度 更贴近数学定义 需要显式控制变量
性能 函数调用有开销 通常更快
内存风险 可能栈溢出(见下文) 可控
适用场景 树/图遍历、分治算法 简单线性重复

小心"栈溢出":递归的代价

每次函数调用,系统都会在"调用栈"上压入一个帧(包含参数、局部变量)。递归过深(比如 factorial(100000))会导致栈空间耗尽,程序崩溃。

如何避免?

  • 设定合理的递归深度限制(通常几千层以内安全)

  • 改用尾递归优化(部分语言支持)

  • 用循环或显式栈重写

尾递归示例(仅限支持优化的环境)

javascript

复制代码
// 尾递归:最后一步只调用自身,不附加运算
function factorialTail(n, accumulator = 1) {
  if (n <= 1) return accumulator;
  return factorialTail(n - 1, n * accumulator);
}
// 某些JS引擎(如Safari)会优化这种写法,避免栈增长

递归的威力:遍历树状结构

递归最擅长的就是处理未知深度、天然嵌套的数据。比如文件目录、评论回复、组织架构。

示例:遍历文件夹(Node.js)

javascript

复制代码
const fs = require('fs');
const path = require('path');

function listAllFiles(dirPath, indent = '') {
  const files = fs.readdirSync(dirPath);
  for (let file of files) {
    const fullPath = path.join(dirPath, file);
    const stat = fs.statSync(fullPath);
    if (stat.isDirectory()) {
      console.log(`${indent}📁 ${file}`);
      listAllFiles(fullPath, indent + '  ');
    } else {
      console.log(`${indent}📄 ${file}`);
    }
  }
}

listAllFiles('./my-project');

递归 vs 分治:一对好搭档

递归是实现分治算法的自然方式:

  1. 分割:把问题分成几个相同形式的子问题

  2. 解决:递归地解决子问题(子问题足够小就直接解)

  3. 合并:把子问题的结果合并成原问题的解

经典案例:归并排序、快速排序、二分查找。

二分查找的递归写法

javascript

复制代码
function binarySearch(arr, target, left = 0, right = arr.length - 1) {
  if (left > right) return -1; // 没找到
  
  const mid = Math.floor((left + right) / 2);
  if (arr[mid] === target) return mid;
  if (arr[mid] > target) return binarySearch(arr, target, left, mid - 1);
  return binarySearch(arr, target, mid + 1, right);
}

const sorted = [2, 5, 8, 12, 16, 23, 38, 56];
console.log(binarySearch(sorted, 23)); // 5

总结:什么时候该用递归?

适合递归的场景

  • 问题天然是递归定义的(如树、图、汉诺塔)

  • 需要深度优先遍历(如解析JSON、渲染嵌套UI组件)

  • 想让代码高度贴合数学公式(如阶乘、斐波那契)

不适合递归的场景

  • 只需要简单重复(用循环更清晰)

  • 递归深度可能超过数千(小心栈溢出)

  • 性能要求极高(函数调用有开销)


动手挑战一下

实现一个函数 flattenArray,将任意嵌套的数组展开成一维数组:

javascript

复制代码
// 输入:[1, [2, [3, 4], 5], 6]
// 输出:[1, 2, 3, 4, 5, 6]

function flattenArray(arr) {
  // 你的递归代码在这里
}

<details> <summary>参考实现(点击展开)</summary>

javascript

复制代码
function flattenArray(arr) {
  let result = [];
  for (let item of arr) {
    if (Array.isArray(item)) {
      result = result.concat(flattenArray(item));
    } else {
      result.push(item);
    }
  }
  return result;
}

</details>


递归就像编程中的"盗梦空间"------只要你信任每一层都能完成自己的任务,整个问题就会像多米诺骨牌一样优雅倒塌。希望这篇文章能帮你打开这扇思维之门。

欢迎在评论区留下你对递归的理解或疑惑,我会抽空回复。

如果你喜欢这种带代码的技术科普,别忘了点赞关注,我们下期见!

相关推荐
GEO从入门到精通1 小时前
在哪里能买到GEO学习工具或课程?
人工智能·学习
测试员周周1 小时前
【Appium 系列】第14节-断言与验证 — Validator 的设计
android·人工智能·python·功能测试·ios·单元测试·appium
心中有国也有家1 小时前
从零上手 CANN 学习中心:像逛技术便利店一样学昇腾
学习·算法·开源
小白|1 小时前
tensorflow:昇腾CANN的TensorFlow适配层
人工智能·python·tensorflow
武汉唯众智创1 小时前
全栈物联网实训平台拆解:通信协议+边缘AI+实战源码
人工智能·物联网·嵌入式开发·物联网实训平台·高校实训·python物联网
oo哦哦1 小时前
搜索矩阵系统的最短路密码:用Dijkstra算法和网络流理论,解释为什么你做了1000个关键词,流量还不如别人30个
网络·算法·矩阵
Matlab程序猿小助手1 小时前
【MATLAB源码-第319期】基于matlab的帝王蝶优化算法(MBO)无人机三维路径规划,输出做短路径图和适应度曲线.
开发语言·算法·matlab
码点滴1 小时前
CRI-O选型与容器运行时标准
开发语言·人工智能·架构·kubernetes·cri-o
一起聊电气1 小时前
智能断路器:守护智能照明系统的AI电气安全闸门
网络·人工智能·安全