递归算法入门:像俄罗斯套娃一样思考
2026/5/23 7:17:02 网站建设 项目流程

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

你有没有拆过俄罗斯套娃?打开一个,里面还有一个,再打开,又一个……直到最小的那个。递归(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>


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

欢迎在评论区留下你对递归的理解或疑惑,我会抽空回复。
如果你喜欢这种带代码的技术科普,别忘了点赞关注,我们下期见!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询