45、如何理解和实现递归?数组扁平化里递归有什么缺陷?

张开发
2026/4/20 18:32:13 15 分钟阅读

分享文章

45、如何理解和实现递归?数组扁平化里递归有什么缺陷?
目录一、先给面试里的标准定义什么是递归二、递归的核心组成1. 终止条件2. 当前层逻辑3. 递归子问题三、如何写递归一个通用思路例子求 1 到 n 的和拆解四、递归的执行过程怎么理解1. 递进2. 回归五、数组扁平化是什么六、数组扁平化如何用递归实现方案一最经典递归写法执行逻辑示例七、数组扁平化递归的思路怎么讲八、递归实现数组扁平化的几种写法写法1concat 版写法2闭包 外部结果数组写法3reduce 版递归九、数组扁平化中递归的缺陷1. 调用栈过深可能栈溢出原因2. 性能开销较大3. concat 写法会产生很多中间数组4. 可控性较差5. 调试复杂度较高十、如何更精彩地说“递归的缺陷”十一、递归的替代方案迭代 栈用栈实现数组扁平化思路解释优点十二、递归和迭代方案怎么对比十三、还有哪些扁平化方案1. Array.prototype.flat优点注意2. toString / split问题十四、面试怎么回答更精彩版本1基础标准版版本2高分版十五、如果面试官继续追问可以这样答追问1如何判断一个问题适不适合递归追问2递归和循环的本质区别是什么追问3数组扁平化为什么适合递归追问4如何优化递归版扁平化十六、最适合背诵的面试模板十七、一句话收尾总结一、先给面试里的标准定义什么是递归递归就是一个函数在函数内部调用自身把一个大问题不断拆解成规模更小、结构相同的子问题直到满足终止条件。这里面有三个关键词自己调用自己子问题和原问题结构相同必须有终止条件二、递归的核心组成面试时如果你想答得有条理可以说一个递归函数通常由三部分组成终止条件当前层逻辑递归处理下一层子问题这是非常标准的回答框架。1. 终止条件如果没有终止条件就会无限调用最终导致栈溢出Maximum call stack size exceeded例如function fn() { fn() } fn()这就是无限递归。2. 当前层逻辑每一层递归不能只“继续调用”还要做当前层该做的事。比如遍历树、求和、扁平化时每层都要处理本层的数据。3. 递归子问题将问题缩小继续交给同一个函数处理。比如处理左子树处理右子树处理子数组处理下一节点三、如何写递归一个通用思路你可以给面试官一个方法论我写递归一般会按 4 步明确函数的定义找到终止条件写出当前层逻辑找到和原问题相同的子问题继续递归这个回答会显得你不是“背代码”而是真的会设计递归。例子求 1 到 n 的和function sum(n) { if (n 1) return 1 return n sum(n - 1) }拆解函数定义sum(n)表示 1 到 n 的和终止条件n 1当前层逻辑当前层要加上n子问题剩下的是sum(n - 1)四、递归的执行过程怎么理解这个点说清楚面试会很加分。以sum(4)为例sum(4) 4 sum(3) 4 3 sum(2) 4 3 2 sum(1) 4 3 2 1 10递归本质上会经历两个过程1. 递进不断调用自己把问题压入调用栈。2. 回归当到达终止条件后再一层层返回结果。所以递归和调用栈关系非常密切。五、数组扁平化是什么比如有这样一个多维数组const arr [1, [2, [3, 4], 5], 6]扁平化之后变成[1, 2, 3, 4, 5, 6]这个问题非常适合用递归因为它天然符合一个数组中可能还包含子数组子数组的结构和原数组一致所以可以继续用同样的方法处理这就是“原问题和子问题结构相同”非常典型的递归场景。六、数组扁平化如何用递归实现方案一最经典递归写法function flatten(arr) { let result [] for (const item of arr) { if (Array.isArray(item)) { result result.concat(flatten(item)) } else { result.push(item) } } return result }执行逻辑如果当前元素不是数组直接放进结果如果当前元素还是数组递归展开它再把结果拼接进来示例const arr [1, [2, [3, 4], 5], 6] console.log(flatten(arr)) // [1, 2, 3, 4, 5, 6]七、数组扁平化递归的思路怎么讲面试里你可以这样说数组扁平化适合递归因为数组里的元素如果是数组本质上就是一个规模更小但结构相同的子问题。所以我会遍历当前数组如果当前项不是数组就直接放入结果如果当前项是数组就递归调用 flatten 继续处理最终把所有层级的结果合并起来。这个表达就很标准。八、递归实现数组扁平化的几种写法写法1concat版function flatten(arr) { let res [] for (const item of arr) { if (Array.isArray(item)) { res res.concat(flatten(item)) } else { res.push(item) } } return res }写法2闭包 外部结果数组function flatten(arr) { const res [] function dfs(list) { for (const item of list) { if (Array.isArray(item)) { dfs(item) } else { res.push(item) } } } dfs(arr) return res }这种方式通常比频繁concat更好一些因为少了很多中间数组。写法3reduce版递归function flatten(arr) { return arr.reduce((prev, cur) { return prev.concat(Array.isArray(cur) ? flatten(cur) : cur) }, []) }这个写法更“函数式”但面试里建议先讲普通版更清晰。九、数组扁平化中递归的缺陷这个才是这题真正的重点。很多人只会写递归但面试官更想知道你知道递归会带来什么问题吗1. 调用栈过深可能栈溢出这是递归最大的缺陷之一。如果数组嵌套层级特别深比如let arr [1] for (let i 0; i 100000; i) { arr [arr] }再调用递归版flatten(arr)很可能会报错Maximum call stack size exceeded原因递归每调用一次都会压入一个函数栈帧。JS 的调用栈大小是有限的所以嵌套太深就会爆栈。2. 性能开销较大递归有额外的函数调用成本创建调用栈帧参数传递返回值回收如果数据量很大、层级很多递归往往不如迭代方式稳定。3.concat写法会产生很多中间数组比如这个版本res res.concat(flatten(item))每次concat都可能创建一个新数组带来额外内存分配数据复制成本性能下降所以如果扁平化数组很大这种写法性能不够理想。4. 可控性较差递归虽然简洁但在一些场景下不如循环 / 栈结构那样容易控制不好做深度限制不好做中途暂停不好做超大数据分批处理5. 调试复杂度较高递归调用层级一深调试时容易看懵当前在哪一层哪一步返回了为什么结果重复 / 丢失特别是对于递归不熟的人出 bug 不容易定位。十、如何更精彩地说“递归的缺陷”面试时你可以这样说递归实现数组扁平化的优点是代码简洁、思路自然因为嵌套数组本身就具有递归结构。但它也有明显缺点第一嵌套层级过深时可能导致调用栈溢出第二递归有额外的函数调用开销第三如果用concat合并结果会创建很多中间数组性能和内存开销都比较大。所以在数据量大或者嵌套层级不确定的场景下我会更倾向于使用迭代 栈的方式来实现。这段就非常像成熟回答。十一、递归的替代方案迭代 栈如果面试只会递归通常还不够。你最好顺带说一个非递归解法非常加分。用栈实现数组扁平化function flatten(arr) { const stack [...arr] const res [] while (stack.length) { const item stack.pop() if (Array.isArray(item)) { stack.push(...item) } else { res.push(item) } } return res.reverse() }思路解释先把原数组元素放进栈里每次取出一个元素如果它是数组就继续展开压回栈如果不是数组就加入结果因为栈是后进先出最后要reverse()优点不依赖函数递归不容易爆调用栈更适合处理深层嵌套十二、递归和迭代方案怎么对比方案优点缺点递归代码简洁符合问题结构易于表达深层嵌套可能栈溢出函数调用开销大迭代 栈更稳定可控性强不容易爆栈写法相对复杂可读性略低十三、还有哪些扁平化方案如果面试官继续追问你还可以提1.Array.prototype.flatarr.flat(Infinity)例如const arr [1, [2, [3, 4], 5], 6] console.log(arr.flat(Infinity))优点简洁原生支持注意老环境兼容性要考虑面试一般更关注你手写实现能力2.toString / split这个可以提但不建议作为正规答案。arr.toString().split(,)问题所有值都会变成字符串不适合对象、null、undefined、布尔值等复杂情况所以这只是“技巧”不是严肃的通用实现。十四、面试怎么回答更精彩下面给你几个版本。版本1基础标准版递归本质上是函数调用自身把大问题拆解成结构相同的子问题。实现递归时我一般会先确定三件事函数定义、终止条件、当前层逻辑。以数组扁平化为例如果当前元素不是数组就直接放入结果如果当前元素还是数组就递归继续处理它这样就能不断把多维数组展开成一维数组。不过递归也有缺点尤其在数组扁平化场景里如果嵌套层级太深会导致调用栈溢出另外递归有额外函数调用开销如果使用concat还会产生很多中间数组影响性能。所以在层级较深或者数据量较大的场景下我会考虑用迭代 栈来替代递归实现。版本2高分版我理解递归的核心不是“函数自己调自己”这么简单而是把一个问题拆解为若干个规模更小但结构相同的子问题。所以写递归时我通常会先明确四点函数的定义是什么终止条件是什么当前层要做什么哪一部分可以继续递归。在数组扁平化里这个结构非常典型数组中的某一项如果还是数组它本身就是一个更小的扁平化问题所以很适合递归处理。实现上我会遍历当前数组遇到普通值就直接收集遇到子数组就递归展开。但递归在这个场景里也有几个明显缺陷第一嵌套层级深时容易触发调用栈溢出第二递归本身有函数调用开销第三如果用concat合并结果会频繁创建中间数组导致额外的时间和空间消耗。所以如果只是代码表达清晰递归是很自然的方案但如果我考虑稳定性和大数据量场景会更倾向于使用迭代 栈实现避免深递归带来的风险。十五、如果面试官继续追问可以这样答追问1如何判断一个问题适不适合递归我一般会看两个条件第一问题能不能拆成规模更小但类型相同的子问题第二是否存在明确的终止条件。如果这两个条件都满足通常就比较适合递归。追问2递归和循环的本质区别是什么递归是通过函数调用栈隐式地维护状态循环则通常是显式地通过变量或数据结构维护状态。递归表达力强适合树、链表、分治这类天然分层结构循环通常性能和可控性更好。追问3数组扁平化为什么适合递归因为数组中的元素如果还是数组那么它就是原问题的一个子问题结构完全一致这种“自相似结构”非常适合递归。追问4如何优化递归版扁平化你可以答可以从两个方向优化不用concat改成外部结果数组 push减少中间数组创建如果担心层级太深就改成显式栈的迭代实现从根本上规避调用栈问题。例如优化版递归function flatten(arr) { const res [] function dfs(list) { for (const item of list) { if (Array.isArray(item)) { dfs(item) } else { res.push(item) } } } dfs(arr) return res }十六、最适合背诵的面试模板递归的本质是把一个大问题拆成规模更小但结构相同的子问题所以实现递归时我一般会先明确函数定义、终止条件、当前层逻辑以及递归子问题。以数组扁平化为例遍历数组时如果当前项不是数组就直接收集如果当前项还是数组就递归继续展开因为子数组本身就是一个更小的扁平化问题。不过递归在这个场景下也有缺点第一嵌套过深时容易导致调用栈溢出第二递归有额外函数调用开销第三如果实现里使用concat还会频繁产生中间数组影响性能。所以我认为递归适合表达思路、代码也更自然但在大数据量或深层嵌套场景下我会更倾向于用迭代 栈来实现稳定性更好。十七、一句话收尾总结递归适合解决“结构自相似”的问题数组扁平化就是典型例子但递归的代价是栈深限制和额外开销所以面试里最好同时会讲递归实现和非递归优化方案。

更多文章