说个让我印象深刻的面试经历。

去年冬天,面试一个工作两年的候选人。简历上写着"熟悉运动学算法",项目里做过六轴机械臂的轨迹规划。我想着这种人,递归应该不在话下。

结果我问了三个问题,他一个都没答上来。

第一个:"什么是递归?"他说:"函数调用自己。"——对,但只说对了一半。

第二个:"你在运动学项目里用过递归吗?"他想了想说:"用过,但记不清具体场景了。"

第三个:"递归深度太大会有什么问题?"他犹豫了半天,说了句"可能会崩"。

讲真,我当时的表情大概是这样的:😐

不是为难他。递归这个东西,课本上教的是阶乘、斐波那契,但面试的时候,特别是机器人方向的面试,考的东西比这深得多。

而且递归特别能看出一个人的思维深度。能讲清楚递归的人,通常对函数调用栈、内存管理、算法设计都有不错的理解。讲不清楚的,后面问项目经验也会打折扣。

递归到底是什么?别只说"函数调用自己"

"函数调用自己"这个定义没错,但太浅了。

递归的本质是什么?把一个复杂问题拆成结构相同的子问题,直到子问题简单到可以直接求解。

换句话说,递归需要两样东西:递推关系终止条件。没有递推关系,你不知道怎么拆;没有终止条件,你就永远拆下去,直到栈溢出。

拿最经典的阶乘来说:

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 vector<double>& angles) {
    if (jointIndex == 0) {
        return basePose;  // 基座坐标
    }
    Pose parentPose = forwardKinematics(jointIndex - 1, angles);
    return parentPose * transformFromAngle(jointIndex, angles[jointIndex]);
}

每一级关节的位姿,都依赖上一级的结果。这种"层层依赖"的关系,天然适合递归来表达。

再比如逆运动学,有些解法也用递归。从末端目标位姿反推每个关节角度,一层层往回推。

除了运动学,递归在机器人里还有这些应用场景:树结构遍历(比如场景图、机器人本体的URDF模型树——一个机械臂有base、link、joint层层嵌套,遍历它天然就是递归)、分治算法(比如点云处理的八叉树,把三维空间递归地分成八块来加速近邻搜索)、回溯搜索(比如路径规划里的DFS,在栅格地图上递归地探索可行路径)。

面试的时候,如果你能结合机器人场景讲递归,而不是只说阶乘和斐波那契,面试官会觉得你有实际项目经验。

递归的致命问题:栈溢出

刚才那个候选人说"递归太深可能会崩",其实说的就是栈溢出。

每次函数调用,系统都会在栈上分配一块空间(栈帧),保存局部变量、参数、返回地址。栈的大小是有限的,Linux下默认8MB,Windows默认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——自定义数据类型的第一步

有任何问题欢迎评论区留言,我会尽量回复。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐