资讯动态

递归层序遍历与有序数组转平衡BST:两道题吃透二叉树核心思维

发布时间:2026/9/9 7:10:21 来源:尧图企业网站定制
前几天有位读者跟我抱怨说面试官让他“用递归实现二叉树的层序遍历”他当时就愣住了。在他的认知里层序遍历等于队列加 BFS 循环递归是 DFS 的专属玩法。这个想法其实很典型也是大多数人刷二叉树题时的一道隐形天花板。等我把思路讲完他才发现原来递归和层序遍历并不冲突甚至代码会比迭代写法更短。而另一道配套的“有序数组转平衡 BST”看起来是构造题其实考的完全是同一套递归思维。这两道题放在一起刷收益远大于分开刷。这两道题的价值在于它们都不是靠背模板能解决的。第一题打破“BFS 必须配队列”的思维定式第二题要求你从 BST 的中序性质反推构造逻辑。两道题都涉及二叉树、递归、BFS、层序遍历、BST 这些高频考点而且代码量极小非常适合在面试现场快速写出。无论你是准备校招、社招还是单纯想把树的递归思维练扎实这篇内容都值得看完。1. 面试官为什么偏爱这两道题它根本不是考“你会不会背”很多人刷题有个误区觉得二叉树题就是背三种遍历模板层序遍历背队列版本前中后序背递归版本。但面试官出这两道题真正想看的不是你能否默写代码而是你对递归的本质、树的形状、搜索顺序这些底层概念有没有想透。1.1 层序遍历的“递归”要求其实是考对 DFS 和 BFS 本质的理解传统层序遍历用队列这是教科书写法。它的核心思想是先处理当前层的所有节点再把它们的子节点收集起来作为下一层。这个过程天然是“广度优先”的一层一层往外扩。但如果你问一句“层序遍历的本质是什么”答案其实不是“必须用队列”而是“按深度从小到大输出节点”。只要最终结果满足每一层内部从左到右、层与层之间从上到下它就是层序遍历。想通这一点递归方案就呼之欲出了递归天然有“深度”这个参数每次进入下一层递归深度加一。我们只需要在递归过程中把节点按深度记到对应的桶里最后所有桶拼起来就是层序遍历的结果。面试官之所以喜欢这样问是因为它能区分“背模板的人”和“理解原理的人”。背模板的人听到递归会卡壳理解原理的人会立刻意识到递归的深度就是层号深度相同的节点自然归到同一层。1.2 有序数组转平衡 BST考的是“从数据结构性质反推算法”第二题更典型。给定一个升序数组构造一棵高度平衡的二叉搜索树BST。面试官想听到的第一反应不是“我背过这题”而是“BST 的中序遍历结果就是升序数组”。这是 BST 最重要的性质中序遍历一棵 BST得到的节点值序列一定是严格递增的。有了这个性质问题就变得清晰了数组相当于这棵 BST 的中序遍历结果。但仅有中序遍历结果并不能唯一确定一棵二叉树。比如 [1, 2, 3] 这个序列可以是根为 1、右子树为 2 再右子树为 3 的斜树也可以是根为 2、左右孩子分别为 1 和 3 的满二叉树。两者中序遍历结果都是 [1, 2, 3]。所以这道题真正的考点是在众多可能的树形里如何通过“取中点作为根”这个策略强制构造出一棵高度平衡的树。取中点意味着左右子树的节点数最多相差一个递归下去任意子树都是如此。这正是“平衡”二字的来源不是因为题目要求平衡所以取中点而是取中点这个操作天然保证了平衡。这两道题放在一起非常妙。第一题考你“递归能不能模拟出层序”第二题考你“树的性质能不能反推构造”。一个正着推一个倒着推刚好把二叉树的核心思维练了个遍。2. 递归实现层序遍历用先序递归的壳装层序遍历的魂递归实现层序遍历我见过最简洁的版本只有十行左右。核心思路极其简单把递归深度当成层号。2.1 核心思路递归深度就是层号先序遍历的访问顺序是“根、左、右”。递归每往下走一层深度加一。如果我们在递归函数里维护一个 depth 参数那么任意节点的 depth 就代表了它在树中的层数也就是层序遍历中的下标。具体做法是准备一个二维数组 res。当第一次访问到第 depth 层时先往 res 里追加一个空列表然后把当前节点的值追加到 res[depth] 中。递归处理左右子树时depth 加一。这里的关键点在于不管递归顺序是先序还是中序最终 res 的每一层收集到的节点集合是不变的。因为同一深度的节点一定会被分配到同一个列表里。而先序遍历能额外保证每一层内部的顺序是从左到右的因为左侧节点总是先于右侧节点被访问。这一点面试时值得主动讲出来它是这个解法“为什么正确”的底层依据。2.2 代码逐行拆解以 Python 为例完整代码如下from typing import List, Optional class TreeNode: def __init__(self, val0, leftNone, rightNone): self.val val self.left left self.right right def levelOrder(root: Optional[TreeNode]) - List[List[int]]: res [] def traverse(node: Optional[TreeNode], depth: int) - None: if not node: return if len(res) depth: res.append([]) res[depth].append(node.val) traverse(node.left, depth 1) traverse(node.right, depth 1) traverse(root, 0) return res逐行看一下if not node: return处理空节点也是递归出口。if len(res) depth: res.append([])表示第一次触达这一层。因为最开始 res 为空当 depth 从 0 开始递增时len(res) 永远等于当前已经创建的最大层数所以当len(res) depth时说明这一层还没有列表需要先创建。res[depth].append(node.val)把当前节点值放进对应层的列表。然后递归处理左右子树深度加一。对于下面这棵树1 / \ 2 3 / \ \ 4 5 6递归过程会先走左子树依次访问 1、2、4此时 res 变为[[1], [2], [4]]。然后回溯到 2访问 5res 变为[[1], [2], [4, 5]]。再回溯到 1访问右子树 3res 变为[[1], [2, 3], [4, 5]]最后访问 6res 变成[[1], [2, 3], [4, 5, 6]]。注意第二层的 3 是在 5 之后才被加入的但因为 3 在第二层它会被追加到res[1]中顺序是 2 在前、3 在后。所以最终结果依然满足层序从左到右。2.3 为什么这个“递归 BFS”是高效的时间复杂度是 O(n)每个节点恰好访问一次。空间复杂度如果不算结果数组递归调用栈的深度是 O(h)h 是树的高度。最坏情况下树退化成链表递归深度是 O(n)这一点和所有递归写法一样需要注意栈溢出问题。对比一下经典的队列迭代版维度队列 BFS 迭代版递归 depth 版遍历顺序严格广度优先实际是深度优先但按层存储结果额外空间需维护队列最大宽度 O(w)递归栈 O(h)无队列代码长度约 1520 行约 10 行层内顺序天然从左到右由先序递归保证从左到右面试官视角教科书标准解法体现对递归和 BFS 的理解严格来说这个递归版本并不是“广度优先搜索”——它没有像 BFS 那样逐层扩展节点。它只是通过深度参数把 DFS 的访问结果按照层级重新组织。但最终产出的序列和层序遍历完全一致。在面试场景里你能把这个区别讲明白比报一个“递归 BFS”的名头更能加印象分。注意如果面试官追问“能不能写出真正递归的 BFS”也就是每一层用一个队列传入递归函数也可以实现但代码复杂度会明显上升而且容易出错。实际面试中先写 depth 版即可绝大多数面试官认可这个解法。3. 递归层序遍历的边界与变形从空树到锯齿形遍历代码短不代表不用测试。层序遍历看似简单边界条件和细节坑不少。我把自己踩过的和帮别人 review 时发现的问题汇总在这里。3.1 空树、单节点、斜树这些用例必须心里有数先说空树。root为 None 时traverse(root, 0)进来直接 returnres 保持空列表最终返回[]没毛病。这是第一个测试用例。单节点树res 初始为空depth 为 0因为len(res) 0创建res [[]]然后 append 根节点值返回[[val]]正确。斜树比如每个节点只有左孩子。递归会一直往左走res 的每一层只有一个元素。因为递归先处理左子树再处理右子树右子树为空时直接 return所以结果不会多出奇怪的空列表。这个细节很关键只要你没有在递归里对空节点做“创建层”的逻辑就不会出现[[], []]这种情况。还有完全二叉树和满二叉树这个解法的输出顺序也正确。我建议你把这几种结构画出来手动跑一遍递归比直接看代码印象深刻得多。3.2 从层序结果反推为什么节点顺序是对的有个隐藏问题值得深挖递归是深度优先的为什么同层节点不会出现右节点跑到左节点前面的情况答案在递归顺序里。先序遍历先处理根再处理左子树最后处理右子树。任意两个处于同一层的节点 A 和 B如果 A 在 B 的左侧那么在树结构上访问 A 的路径一定先于访问 B 的路径。因为 B 只能在 A 的某个祖先的右子树方向出现而递归会先把左子树整棵处理完才会回溯去处理右子树。所以 A 一定先被加入res[depth]。这也是为什么这个解法能拿到“层序遍历”的头衔。如果你把顺序改成“根、右、左”那每层的结果就会变成从右到左那就是锯齿形遍历的一种变体了。3.3 锯齿形遍历的扩展层序遍历最常见的变种是 Zigzag Level Order Traversal也就是之字形遍历第一层从左到右第二层从右到左第三层再从左到右。在递归 depth 版里扩展这个功能只需要加一个判断当 depth 是奇数时反转该层的列表或者插入时往头部插。代码如下def zigzagLevelOrder(root: Optional[TreeNode]) - List[List[int]]: res [] def traverse(node: Optional[TreeNode], depth: int) - None: if not node: return if len(res) depth: res.append([]) if depth % 2 0: res[depth].append(node.val) else: res[depth].insert(0, node.val) traverse(node.left, depth 1) traverse(node.right, depth 1) traverse(root, 0) return res这里用了insert(0, val)来保证奇数层从右往左。注意insert(0, ...)的时间复杂度是 O(k)所以这个写法的总复杂度在极端情况下会到 O(n²)。应付面试没问题但如果想追求最优可以先按层序收集完再对奇数层整体反转时间复杂度回到 O(n)。这个优化思路面试时提一嘴是很好的加分项。4. 有序数组转平衡 BST中点分割的数学依据与代码实现第二道题是构造题但构造题不等于随便写。递归的每一步都必须有依据而这道题的依据就是“中序遍历有序”和“中点分割保证平衡”。4.1 为什么一定要选中点高度平衡的递推证明BST 的定义是左子树所有节点值小于根右子树所有节点值大于根。给定一个升序数组数组中任意一个元素都可以作为根只要它左边的元素构成左子树、右边的元素构成右子树就能构造出一棵合法的 BST。但合法不够题目要求“高度平衡”。LeetCode 对高度平衡的定义是“每个节点的左右子树高度差不超过 1”。为了满足这个条件必须让左右子树的节点数尽量接近。取数组的中点作为根左边有 ⌊(n-1)/2⌋ 个元素右边有 ⌈(n-1)/2⌉ 个元素最多差一个。然后递归地对左右两个子数组做同样操作每一层都平衡整棵树自然平衡。你可能会问为什么取中点就一定平衡可以用归纳法想。假设一棵包含 n 个节点的子树T(n) 表示它的最大高度。因为左右子树节点数之差不超过 1所以 T(n) 1 max(T(⌊(n-1)/2⌋), T(⌈(n-1)/2⌉))。可以证明 T(n) ⌈log₂(n1)⌉ 左右也就是说高度稳定在 O(log n)。这就是“中点分割”的数学保证。4.2 三种写法对比切片版、双指针版、迭代版最常见也最简短的写法是 Python 切片版def sortedArrayToBST(nums: List[int]) - Optional[TreeNode]: if not nums: return None mid len(nums) // 2 root TreeNode(nums[mid]) root.left sortedArrayToBST(nums[:mid]) root.right sortedArrayToBST(nums[mid1:]) return root这个版本之所以流传广是因为它把“分治”表达得最直白取中点建根左数组建左子树右数组建右子树三行搞定。但切片会额外产生新数组。严格分析下来每一层递归都会拷贝总长度为 O(n) 的数组递归深度 O(log n)所以总时间复杂度是 O(n log n)。LeetCode 的数据量下能过但面试时考官如果较真这个写法会扣分。更专业的写法是双指针版不产生额外数组def sortedArrayToBST(nums: List[int]) - Optional[TreeNode]: def build(left: int, right: int) - Optional[TreeNode]: if left right: return None mid (left right) // 2 root TreeNode(nums[mid]) root.left build(left, mid - 1) root.right build(mid 1, right) return root return build(0, len(nums) - 1)这个版本的关键是left right作为递归出口而不是left right。很多人在写的时候会漏掉left right的情况导致空数组或单个元素时越界。迭代版用栈模拟递归也可以写但代码明显变长而且意义不大。构造题天然适合递归面试时优先写双指针递归版既体现严谨性又易于解释。4.3 偶数长度数组的隐藏考点两种 mid 都能过数组长度是偶数时比如 [1, 2, 3, 4, 5, 6]中点是len(nums) // 2 3也就是元素 4。左子数组 [1, 2, 3]右子数组 [5, 6]。左右节点数分别是 3 和 2差为 1合法平衡树。但如果你用(left right 1) // 2也就是上取整中点会落在元素 3 上。左子数组 [1, 2]右子数组 [4, 5, 6]同样是 2 和 3 的分布差为 1依然合法。所以两种取法都能构造出高度平衡的 BST只是树的形状不同。LeetCode 的判题器只验证是否满足 BST 性质和平衡条件不要求唯一的树形所以两种都能通过。面试时遇到这个细节可以主动向面试官说明左中位和右中位都满足平衡条件我选择左中位是为了和二分查找的写法保持一致代码更统一。5. 两道题的隐藏关联序列化与反序列化的微缩模型把这两道题连着看你会发现它们其实是同一个问题的正反两面层序遍历结果是一种树的序列化形式有序数组是 BST 的中序序列化形式。理解这层关系对做树的序列化、反序列化题目非常有帮助。5.1 层序遍历序列化需要 null 占位才能重建第一题的输出是一个二维数组比如[[1], [2, 3], [4, 5, 6]]。仅仅靠这个二维数组其实不能唯一重建一棵二叉树。因为你不知道哪些位置有节点哪些位置是空的。比如第二层有 2 和 3但 2 可能有左右子节点也可能没有这些信息在二维数组里丢失了。所以标准的层序序列化通常会把空节点也记录下来用 null 占位。比如1 / \ 2 3 \ 4对应的层序序列化是[1, 2, 3, null, 4, null, null]。有了这些 null才能根据“数组下标与父子节点下标”的对应关系重建树。理解这个你就知道第一题的输出其实是一个“压缩过的层序信息”。面试时被问到“怎么从层序遍历重建二叉树”你要能立刻回答“需要补全空节点或者借助额外标记”。5.2 BST 的中序序列化有序但无法唯一重建第二题的有序数组来自 BST 的中序遍历但它同样无法唯一确定一棵 BST。原因前面说过中序序列只提供了节点值的相对顺序没有提供树的形状。比如有序数组 [1, 2, 3]下面几棵树的中序遍历都是 [1, 2, 3]根为 1右孩子为 22 的右孩子为 3。根为 1右孩子为 33 的左孩子为 2。根为 2左孩子为 1右孩子为 3。所以“有序数组转 BST”这道题本质上是“给定中序遍历额外施加平衡条件反推一棵合法的树”。面试时能说出这一层面试官就知道你不是全靠背题。5.3 如果面试官把两题连起来考我遇到过一种连环问法先让你做有序数组转平衡 BST再让你把生成的树做层序遍历最后问能不能用层序遍历结果还原 BST。这三个问题环环相扣本质上就是把中序、层序、构造、遍历全部串起来。其中最容易翻车的是“用层序遍历结果还原 BST”。层序结果只包含节点值没有空位信息也没有左右子树的明确边界所以需要额外规则才能重建。如果面试官在这里追问你可以说层序结果加 BST 的“左小右大”性质可以用 BFS 的方式从根开始按层把每个节点插到正确的位置但前提是层序数组本身包含所有非空节点且顺序合法。这个连环问没有标准答案考察的是临场推导能力。你能把前两题想透这一关不会太差。6. 实际刷题和面试中的一点个人建议最后聊点刷题之外的东西。代码谁都会写但面试时让代码“显得专业”是另一回事。6.1 先写迭代还是先写递归第一题如果面试官没指定递归我建议你先写递归 depth 版然后主动补一句“这个做法本质是用 DFS 模拟层序结果”再写出队列迭代版做对比。这个动作能展示你掌握两种思路而且理解它们的关系。只写一种面试官会追问不如你主动讲全。第二题我建议直接写双指针递归版并且解释为什么取中点。一定要说出 BST 中序遍历有序这个性质否则面试官会以为你只是在背模板。6.2 怎么在面试现场把“为什么”讲明白我常用的表达逻辑是结论先行再给证明。比如第二题开口先说“因为 BST 中序遍历是升序所以数组相当于中序序列为了满足平衡我每次取中点作为根左右子树递归构造”。这比一上来就写代码好得多。第一题则说“递归深度可以映射为层号所以我用 depth 拆桶收集节点因为先序访问保证左侧节点先访问所以每层顺序正确”。面试官听到这句话基本不会再怀疑你背题。6.3 一个容易翻车的小细节递归深度与调用栈Python 默认递归深度限制在 1000 左右。斜树情况下第一题的递归深度可能达到树的节点数超过限制会直接 RecursionError。这是递归方案在做这道题时的真实风险。如果你明确知道树的规模很大或者面试官特别提到“如果是斜树怎么办”你可以主动说“递归方案在这种情况下有栈溢出风险需要改用迭代 BFS 加队列”。然后现场切到迭代版。这种“主动承认方案局限并给出替代方案”的行为在面试里非常加分。我在实际刷题时会把这两道题绑定在一起练。先做层序遍历再做数组转树然后用层序遍历验证生成的树。整个过程不到二十分钟但把递归、BFS、层序、BST 这些高频知识点全部过了一遍。如果你觉得自己的二叉树基础还不够扎实我建议你也试试这个组合练法比单独刷十道同类题效率高得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价