递归函数面试三连问:什么是递归、什么时候用、怎么避免栈溢出
说个让我印象深刻的面试经历。去年冬天面试一个工作两年的候选人。简历上写着熟悉运动学算法项目里做过六轴机械臂的轨迹规划。我想着这种人递归应该不在话下。结果我问了三个问题他一个都没答上来。第一个什么是递归他说函数调用自己。——对但只说对了一半。第二个你在运动学项目里用过递归吗他想了想说用过但记不清具体场景了。第三个递归深度太大会有什么问题他犹豫了半天说了句可能会崩。讲真我当时的表情大概是这样的不是为难他。递归这个东西课本上教的是阶乘、斐波那契但面试的时候特别是机器人方向的面试考的东西比这深得多。而且递归特别能看出一个人的思维深度。能讲清楚递归的人通常对函数调用栈、内存管理、算法设计都有不错的理解。讲不清楚的后面问项目经验也会打折扣。递归到底是什么别只说函数调用自己函数调用自己这个定义没错但太浅了。递归的本质是什么把一个复杂问题拆成结构相同的子问题直到子问题简单到可以直接求解。换句话说递归需要两样东西递推关系和终止条件。没有递推关系你不知道怎么拆没有终止条件你就永远拆下去直到栈溢出。拿最经典的阶乘来说int factorial(int n) { if (n 1) return 1; // 终止条件 return n * factorial(n - 1); // 递推关系 }n 1就是终止条件n * factorial(n-1)就是递推关系。两个缺一不可。我见过有人写递归忘了写终止条件程序直接崩了。还见过终止条件写错的比如写成n 1结果传入负数无限递归。面试的时候如果你只说函数调用自己面试官会觉得你只会背定义。但如果你能说出递推关系和终止条件这两个关键词印象分直接拉满。机器人开发里递归到底用在哪很多人觉得递归就是课本里的数学题实际工程里用不上。错了。在机器人开发里递归用得最多的地方是运动学求解。比如你有一个六轴机械臂每个关节的角度已知要求末端执行器的位置。这就是正向运动学。你可以用递推的方式从第一个关节算到第六个关节Pose forwardKinematics(int jointIndex, const vectordouble angles) { if (jointIndex 0) { return basePose; // 基座坐标 } Pose parentPose forwardKinematics(jointIndex - 1, angles); return parentPose * transformFromAngle(jointIndex, angles[jointIndex]); }每一级关节的位姿都依赖上一级的结果。这种层层依赖的关系天然适合递归来表达。再比如逆运动学有些解法也用递归。从末端目标位姿反推每个关节角度一层层往回推。除了运动学递归在机器人里还有这些应用场景树结构遍历比如场景图、机器人本体的URDF模型树——一个机械臂有base、link、joint层层嵌套遍历它天然就是递归、分治算法比如点云处理的八叉树把三维空间递归地分成八块来加速近邻搜索、回溯搜索比如路径规划里的DFS在栅格地图上递归地探索可行路径。面试的时候如果你能结合机器人场景讲递归而不是只说阶乘和斐波那契面试官会觉得你有实际项目经验。递归的致命问题栈溢出刚才那个候选人说递归太深可能会崩其实说的就是栈溢出。每次函数调用系统都会在栈上分配一块空间栈帧保存局部变量、参数、返回地址。栈的大小是有限的Linux下默认8MBWindows默认1MB。递归太深栈帧叠加超出栈大小程序就崩了。那递归多深会溢出这取决于每次调用用了多少栈空间。我给你算一笔账。假设每次递归调用占512字节的栈空间8MB的栈最多支持大约16000层递归。听起来很多但在某些场景下比如搜索一棵很深的决策树16000层真不一定够用。怎么避免栈溢出方法一用迭代代替递归。任何递归都可以改写成迭代用一个栈或队列手动管理状态。// 递归版本 int factorial(int n) { if (n 1) return 1; return n * factorial(n - 1); } // 迭代版本 int factorial(int n) { int result 1; for (int i 2; i n; i) { result * i; } return result; }迭代版本没有栈溢出的风险因为只用了一个栈帧。方法二尾递归优化。如果递归调用是函数的最后一个操作编译器可以优化它复用当前栈帧。int factorialTail(int n, int acc 1) { if (n 1) return acc; return factorialTail(n - 1, n * acc); // 尾递归 }但要注意C标准不强制要求编译器做尾递归优化。GCC和Clang在-O2以上会优化但MSVC不一定。所以在实际工程中不能依赖尾递归优化。在机器人开发里我对递归的态度是能用迭代就用迭代除非递归能让代码清晰很多。毕竟机器人程序对稳定性要求很高栈溢出这种风险能避免就避免。我之前遇到过一个真实的case。一个同事写了一个递归函数来遍历机器人的关节链代码写得挺漂亮的。但在测试的时候偶尔会core dump。查了半天发现是某个关节链特别长递归深度超过了栈的限制。最后改成迭代版本问题就消失了。所以你看递归在工程里不是不能用而是要心里有数。递归vs迭代到底选哪个优先用递归的场景树和图的遍历DFS、分治算法、回溯搜索。这些场景用递归写出来代码直观改成迭代反而复杂。优先用迭代的场景简单线性递推、性能敏感的内层循环、嵌入式系统栈空间有限。判断标准递归深度可预测且有上限用递归没问题。深度不可预测一定要用迭代。在机器人开发里最常见的翻车是递归做路径搜索小地图没问题大地图栈溢出。所以工程里稳定性比代码简洁更重要。面试中的递归三连问总结一下面试中关于递归面试官最爱问的三个问题第一问什么是递归不要只说函数调用自己。要说把问题拆成结构相同的子问题需要递推关系和终止条件。第二问递归有什么优缺点优点是代码简洁、直观适合处理树形结构或分治问题。缺点是有函数调用开销深度太大会栈溢出。第三问怎么优化递归两种方法改成迭代或者用尾递归但要说明编译器不一定优化。如果面试官追问你在项目里用过递归吗这时候你就能结合机器人的场景来回答了——运动学求解、URDF树遍历、八叉树点云处理。比只说我用过阶乘强一百倍。给正在准备面试的你一点建议递归这个东西看着简单其实很考功底。我面试的时候如果候选人在递归这个问题上能讲清楚递推关系、终止条件、栈溢出的原因还能结合项目场景举例我基本就认定这个人C基础是扎实的。怎么练我的建议是先把经典的递归题目写一遍——阶乘、斐波那契、汉诺塔、二叉树遍历。然后试着把它们都改写成迭代版本。最后去找一个机器人相关的场景比如正运动学用递归实现一遍。做完这些你对递归的理解就不只是课本上的语法了而是真正能用到工程里的技能。还有一点很重要练习的时候养成画递归树的习惯。每次写递归之前先在纸上画出递归调用的展开过程——第一层做什么、第二层做什么、什么时候终止。画清楚了再写代码能避免80%的递归bug。我在面试中见过太多人上来就写代码写到一半发现终止条件不对推倒重来。先想清楚再动手效率反而更高。下一篇讲结构体struct——自定义数据类型的第一步。从基本类型到组合类型这是C面向对象编程的起点。如果这篇文章对你有帮助欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。「机器人软件开发面试·从入门到精通」连载系列上一篇第7篇 值传递vs引用传递——面试高频陷阱题 下一篇预告第9篇 结构体struct——自定义数据类型的第一步有任何问题欢迎评论区留言我会尽量回复。