news 2026/10/8 10:40:03

二叉树算法入门:递归遍历、层序与深度计算实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
二叉树算法入门:递归遍历、层序与深度计算实战指南

算法训练营进行到第十三天,终于轮到二叉树了。说实话,前面数组、链表、栈、队列学下来,很多人的心态是"数据结构也就这么回事",直到开始写二叉树才真正体会到什么叫"递归的恐惧"。这篇文章就把我训练营期间关于二叉树学习、刷题和踩坑的经验整体梳理一遍,从基本概念到遍历方法,从深度计算到常见报错,尽量用大白话讲清楚。不管是正在学算法的学生,还是准备面试跳槽的工程师,希望这篇文章能帮你把二叉树这块硬骨头啃下来。

1. 二叉树到底是什么:从概念到本质理解

1.1 二叉树为什么是算法学习的"分水岭"

先给没接触过二叉树的朋友扫个盲。二叉树是一种树形结构,每个节点最多只有两个子节点,分别叫左孩子和右孩子。这个"最多两个分支"的限制看起来很苛刻,但正是这个限制让二叉树变得无比重要。数组和链表都是线性结构,数据一个一个排着队,而二叉树首次引入了"层级"和"分支"的概念,这带来的认知升级是巨大的。

我训练营的导师说过一句话,印象很深:"你如果能把二叉树学明白,后面图的算法基本就是套模板;学不明白,DFS和BFS永远都是背代码。"这句话不夸张。因为二叉树的遍历本质上就是深度优先搜索(DFS)和广度优先搜索(BFS)的缩影。前序、中序、后序遍历就是DFS的三种变体,层序遍历就是BFS。树的结构把"递归"这个抽象概念具象化了,所以很多人在二叉树这里第一次真正理解了递归,也有人在这里彻底卡住。

我在训练营里观察到,二叉树学得好的同学都有一个共同特点:他们不急着背代码,而是先在纸上画树。一个三层的二叉树画出来,每个节点的指针关系一目了然,递归调用的过程也能顺着箭头走一遍。这个习惯非常关键。

1.2 存储方式选型:链式结构还是数组结构

二叉树在代码里有两种主流存储方式,一种是链式存储,另一种是顺序存储(数组)。

链式存储很好理解,每个节点就是一个结构体或类,里面存着数据和两个指针:

struct TreeNode { int val; TreeNode* left; TreeNode* right; TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} };

这种写法在LeetCode和面试中最常见,操作灵活,插入和删除方便。你只需要维护根节点指针,剩下的节点通过指针互相连接,想怎么遍历就怎么遍历。

顺序存储则是把二叉树按"完全二叉树"的规则放到数组里:根节点放在下标1(或者0),它的左孩子在下标2*i(或2*i+1),右孩子在2*i+1(或2*i+2)。这种方式的优势是访问速度快、内存连续,但缺点也很明显——如果树是斜树(每个节点只有左孩子或只有右孩子),数组里会浪费大量空间。实际工程中用数组存二叉树的场景不多,就是堆排序里的完全二叉树,还有线段树这种本身就是完全二叉树结构的数据结构。

我的建议是:训练营阶段把链式存储作为主攻方向,把所有遍历和深度计算的题目都用链式结构做一遍。数组存储可以在学到堆排序的时候再回头看,那个时候你会觉得数组存树的设计非常精妙。

提示:链式二叉树中,叶子节点的左右指针都要指向 nullptr。很多人定义节点时忘记初始化指针,运行时直接崩掉,这是最常见的低级错误。C++里建议都用带默认参数的构造函数,Java和Python则在声明时直接赋 null。

2. 二叉树的遍历:核心考点的完整拆解

2.1 前序、中序、后序遍历的理解方式

二叉树的遍历可以说是算法训练营第十三天的重中之重。深度优先遍历(DFS)有三种:前序(先序)、中序、后序。它们的区别就是访问根节点的时机:

  • 前序遍历:根节点 -> 左子树 -> 右子树
  • 中序遍历:左子树 -> 根节点 -> 右子树
  • 后序遍历:左子树 -> 右子树 -> 根节点

很多初学者死记这三句话然后就去做题了,结果遇到"给前序和中序,重建二叉树"这种题,直接懵掉。问题出在理解不够本质。

我的理解方式是这样的:所谓遍历,本质上是把一个非线性结构"压扁"成线性序列。前序、中序、后序只是选择不同的时机把节点加入序列。递归代码相当简单,以中序遍历为例:

def inorder(root): if root is None: return inorder(root.left) print(root.val) inorder(root.right)

注意这个递归的节奏:先一路向左走到最深处,然后逐层回溯。中序遍历的结果对于搜索二叉树来讲是从小到大排好序的,这个特性后面会反复用到。

前序遍历则是"先访问根,再往左钻",所以结果序列的第一个元素一定是整棵树的根节点。后序遍历最后一个元素一定是根节点。这两个性质是"重建二叉树"类题目的核心依据。

我觉得训练营里最有价值的练习方式是:手动模拟递归栈。拿一棵三层的小树,把递归调用当成一摞盘子,每次进入函数就往栈顶压一个节点,返回就弹出一个。用这种方式走上几棵树,递归就不再玄学了。

2.2 层序遍历:队列的应用场景

层序遍历就是广度优先搜索(BFS)在二叉树上的体现,从根开始一层一层往下扫。在力扣上这是最高频的二叉树题目类型之一,因为它后面接了很多变种题:按层收集结果、之字形打印、求每层最大值等等。

层序遍历的经典实现用的是队列:

from collections import deque def levelorder(root): if root is None: return [] result = [] q = deque([root]) while q: level_size = len(q) level = [] for _ in range(level_size): node = q.popleft() level.append(node.val) if node.left: q.append(node.left) if node.right: q.append(node.right) result.append(level) return result

这里有一个非常实操的细节:要在进入每一层之前,先记录level_size = len(q)。为什么?因为队列是在不断变化的,你一边往里加孩子节点,如果不提前锁定当前层的节点数,循环就会把新加入的下一层节点也当成当前层来处理,最后结果全乱。

我见过不少同学在层序遍历这里翻车,问题几乎都出在这一行。之前面试某大厂的时候也问过这个考点,所以一定要记住:按层处理,先记录当前队列长度再出队。

2.3 递归转迭代:栈模拟的通用方法

训练营一定会让你把递归遍历改写成迭代遍历,因为有的面试官明确要求"不要用递归"。递归改迭代的核心思路就一句话:用显式的栈模拟函数调用栈。

拿中序遍历来举例,递归版很好写,迭代版就需要注意"先一路压左,再访问根,再处理右子树":

def inorder_iter(root): result = [] stack = [] cur = root while cur or stack: # 一路向左压栈 while cur: stack.append(cur) cur = cur.left # 弹栈访问 cur = stack.pop() result.append(cur.val) # 转向右子树 cur = cur.right return result

前序遍历改成迭代更容易,因为根节点先访问,直接压栈处理右左即可(注意栈是后进先出,所以先压右再压左):

def preorder_iter(root): if root is None: return [] result = [] stack = [root] while stack: node = stack.pop() result.append(node.val) if node.right: stack.append(node.right) if node.left: stack.append(node.left) return result

后序遍历迭代版稍微绕一点,网上的套路是用两个栈,或者用"前序遍历的变体再反转"。我先说结论:先做根->右->左的遍历,最后把结果反转,就是左->右->根的后序遍历。这个思路我很喜欢,因为理解成本低,写起来还不容易出错。

实操心得:如果面试被问到迭代遍历,优先写前序和中序版本,这两个最直白。后序版本用反转法,三步就能搞定,别去背那种同时维护两个栈的复杂写法,容易临场卡壳。

3. 二叉树的深度与节点计算

3.1 最大深度与最小深度的区别

二叉树深度相关问题在训练营里几乎是必做题,也是热词榜里的高频关键词。力扣第104题"二叉树的最大深度",解法非常典型:

def maxDepth(root): if root is None: return 0 left_depth = maxDepth(root.left) right_depth = maxDepth(root.right) return max(left_depth, right_depth) + 1

递归的思路很清晰:一棵树的深度等于左子树深度和右子树深度的较大值再加一(根节点自己占一层)。这个递归公式理解透了,后面所有跟深度相关的题都能顺手解。

但注意,最小深度没有这么简单。最小深度是从根节点到最近叶子节点的最短路径上的节点数,很多人想当然地写min(left, right) + 1,结果遇到"一棵树只有左子树没有右子树"的情况就翻车了。

比如根节点只有左子树、右子树为空,那么从根到"最近叶子"的路径只能走左边,但min(左子树深度, 0) + 1会得出1,这显然是错的——根节点本身不是叶子,路径至少要到左子树的某个叶子才算数。

正确的写法需要加判断:

def minDepth(root): if root is None: return 0 if root.left is None and root.right is None: return 1 if root.left is None: return minDepth(root.right) + 1 if root.right is None: return minDepth(root.left) + 1 return min(minDepth(root.left), minDepth(root.right)) + 1

这个坑我训练营里踩过,当时想了半天才意识到自己漏了"叶子节点"这个定义。做题之前先搞清楚题目里术语的准确定义,这是训练营第十三天给我最大的收获之一。

3.2 节点计数与平衡判断的统一思路

有了深度的递归公式,很多相关问题都可以举一反三。比如统计节点总数:

def countNodes(root): if root is None: return 0 return countNodes(root.left) + countNodes(root.right) + 1

再比如判断一棵树是否平衡(左右子树高度差不超过1),就是深度与计数的组合应用:

def isBalanced(root): if root is None: return True left_h = getHeight(root.left) right_h = getHeight(root.right) if abs(left_h - right_h) > 1: return False return isBalanced(root.left) and isBalanced(root.right)

这套题目做多了就会发现,二叉树的递归题目全都是"分而治之":先处理根节点(或者不处理),然后递归处理左右子树,最后把结果组合起来。你只要写出"当前节点做什么"和"递归结果怎么合并"这两部分,代码自然就出来了。

这里再补一个我自己总结的做题顺序:拿到二叉树题目先别写代码,先在脑子里回答三个问题。第一,递归的终止条件是什么(一般是节点为空)?第二,当前层要做什么操作?第三,左右子树的结果怎么合并?回答清楚这三个问题,八成以上的二叉树递归题都能解出来。

4. 搜索二叉树与线索二叉树:进阶概念扫盲

4.1 搜索二叉树的特性与应用

搜索二叉树(BST,Binary Search Tree,也叫二叉排序树、二叉查找树)是二叉树里最实用的一种变体。它的定义不复杂:左子树所有节点的值都小于根节点,右子树所有节点的值都大于根节点,并且左右子树本身也是搜索二叉树。

这个特性带来一个惊人的结果:对BST做中序遍历,结果是有序序列。因为中序遍历的顺序是"左-根-右",而BST的性质保证了左 < 根 < 右,所以整个序列天然单调递增。

BST的实际意义在于查找效率。在一棵平衡的BST里查找一个节点,平均只需要 O(log n) 的时间复杂度。这个效率在数据量大的时候非常可观。而且BST支持动态插入和删除,不像数组排序那样需要整体移动,所以它成为很多系统底层结构的基础。

不过BST有个致命问题:如果插入数据的顺序碰巧是有序的,树会退化成一条链表,查找效率直接掉到 O(n)。这就是为什么后面会学到AVL树、红黑树这些自平衡的BST变体。训练营里学BST的时候多留个心眼,把"退化"这个问题记住,后面学平衡树你会理解得更深。

4.2 线索二叉树解决什么问题

线索二叉树在热词里也出现了,这里简单讲一下。线索化要解决的问题是:传统的二叉树节点只有左右孩子指针,做中序(或其他序)遍历的时候必须借助栈或者递归,时间复杂度是O(n)且做不到空间O(1)。但如果给每个节点额外加上"前驱"和"后继"的线索指针,就可以在没有栈的情况下线性遍历二叉树了。

具体做法是利用叶子节点和部分空指针,让原本指向null的左指针指向前驱节点,右指针指向后继节点。这样遍历的时候就相当于走一条"串好的项链",不需要递归和栈。

说句实在话,线索二叉树在面试中出现的频率不高,但在实际工程中它启发了很多设计思路,比如数据库索引的结构优化。训练营里学到它,主要目的是扩充你对"如何优化遍历效率"的认知边界。知道有这么个东西,理解它的基本思路就够了,不用过度投入时间去手写线索化。

4.3 二叉树在真实工程里的应用场景

聊到这里,估计有人会问:二叉树学了到底有啥用?我不能说它直接决定了你的工资,但它在计算机领域就是基础设施级别的存在。

编译器把表达式转换成抽象语法树(AST),本质上是二叉树或类树结构。你要写一个计算器,能用二叉树轻松支持括号和运算符优先级。数据库的索引大量使用B+树,而B+树就是"多路搜索树",和BST的搜索思想一脉相承。操作系统里的文件目录结构是树形结构,路由器转发数据包用到前缀树(Trie,也是多叉树的特例)。堆排序里的堆,本质上就是一颗完全二叉树。

我训练营的讲师说了一句很提气的话:"你学的不是二叉树,是计算机世界里组织和查找数据的元能力。"日常工作中即使你只用CRUD,一旦遇到需要处理层级关系的数据(组织架构、商品分类、评论的回复楼),二叉树这套思维模型会直接帮你快速建模。

5. 写二叉树程序总报运行时错误?排查指南

5.1 空指针解引用:最常见的运行时错误

在热词搜索里,"写二叉树程序时为什么总是报运行时错误"这个搜索频率非常高。我敢说十个二叉树报错,七个是空指针问题。

空指针解引用最常见的一个场景是:访问node.left或node.right时没有检查node本身是否为 null。比如你要打印一棵树的所有节点,写了if (node.left.val > 0)这样的判断,但node.left是空指针,程序直接崩溃。正确的写法要么先判空,要么把空值判断放在递归的最前面统一处理。

另一个常被忽略的场景是修改树的时候用了悬空的临时指针。比如删除BST节点时,把父节点的指针直接指向了待删节点的一个子树,但如果你提前释放了待删节点占用的内存,新的指针就指向了已回收的空间,这种问题在C/C++里尤其隐蔽,有时候不是立刻崩,而是"偶尔崩",排查起来特别痛苦。

我的经验是:每次拿到node指针后,第一件事就是问一句"这个指针能确定非空吗?"不能确定就用条件判断包一层。多写一个if不丢人,少写一个if会丢时间。

5.2 递归边界条件与栈溢出

二叉树递归题的另一个大坑就是递归边界写错,最常见的错误是忘了写终止条件,或者终止条件的位置不对,导致函数无限递归,最终栈溢出。

比如你写一个求和函数:

def sumTree(root): if root is None: return 0 return root.val + sumTree(root.left) + sumTree(root.right)

这个没问题。但如果你把终止条件写成都判断左右孩子,漏掉了对root本身的空判断,当root已经是空节点时,代码还想访问root.left,立刻空指针。所以我的建议是:任何递归函数的开头,第一句就写空值判断,就算某个分支永远走不到,也不要冒险省略。

还有一种栈溢出的情况是树太深。递归版遍历的栈深度等于树的高度,如果这棵树不幸退化成链表(比如1万个节点排成一条线),递归深度就是1万层,很容易爆栈。遇到这种极端情况,要改用迭代版遍历,显式栈放在堆上,安全得多。

5.3 二叉树常见运行时错误速查表

错误现象常见原因排查方向
访问空指针崩溃没判空就访问node.left/right在递归函数开头统一判空
无限递归导致栈溢出递归终止条件缺失或位置错误检查root == None是否在函数最前
结果与预期不符前中后序遍历顺序写混画一棵三层小树手动模拟
层序遍历结果乱序没有记录每层节点数,直接len(q)遍历进入每层前先存level_size = len(q)
修改后树结构丢失指针赋值顺序错误先保存需要保留的指针再改指向
C++内存泄漏删除节点后父指针未置空删除后检查父节点对应指针是否为nullptr

5.4 调试技巧:如何快速定位二叉树Bug

二叉树调试有一个非常实用的土办法:写一个打印函数。训练营阶段别嫌麻烦,先花两分钟写一个能把二叉树按层级结构打印出来的工具函数:

def printTree(root, depth=0): if root is None: return printTree(root.right, depth + 1) print(' ' * depth + str(root.val)) printTree(root.left, depth + 1)

这段代码用中序的方式把树旋转90度打印出来,根节点在左侧,右子树在上方,左子树在下方。每次调完算法,先用这个函数打印一下当前树的真实形态,很多玄学Bug马上就能看出来。我在训练营里就是靠这个工具活过来的——没有可视化工具的年代,它就是最简单直观的"画图调试"。

再补充一个建议:如果做的是修改型操作(比如插入、删除节点),每次只改变一个指针,改完立刻打印,不要攒了一堆操作再调试。小步快跑比大段盲改要高效得多。

6. 针对训练营的二叉树刷题路线与个人建议

6.1 建议刷题顺序:从基础到进阶的清单

训练营第十三天到第十五天的时间是很紧凑的,想一口气把所有内容都学完不现实,建议把刷题按优先级排序。我自己按照这样的顺序来梳理,效果还不错:

  • 基础梯队(必做):二叉树前中后序遍历(递归版)、层序遍历、最大深度、节点个数、反转二叉树。
  • 进阶梯队(尽力做):最小深度、判断平衡、对称二叉树、路径总和。
  • 高阶梯队(按需看):重建二叉树、二叉树最近公共祖先、序列化与反序列化。

这套路线遵循的原则是:先把遍历写熟,再把递归公式套熟,最后才碰状态复杂的题目。很多人第一天上手就做"最近公共祖先",做到怀疑人生,就是因为基础梯队还没稳住。先把简单题刷到条件反射,再上难度,心态和效果都会好很多。

6.2 一个让我茅塞顿开的类比

训练营有个导师用"公司会议"来比喻递归,我觉得这个类比值得分享。想象你是一家公司的CEO,你把任务拆解给两位副总裁(左子树和右子树),让他们各自搞定自己负责的部门。你不需要亲自处理部门里的事,你只需要等他们提交结果,然后汇总。这对应的是后序遍历——先处理左右子树,最后汇总到根。前序遍历则是"老板先定方向,再让下面执行":CEO先拍板,然后副手们按既定方向去做。

这个类比解释了为什么递归代码看起来"什么都没做"却能把问题解决:因为它把具体执行下放给了子调用,自己只负责处理和合并结果。理解了这一层,递归就不再是"玄学"。

6.3 从第十三天往后看:二叉树是地基不是终点

训练营里学二叉树真的不只是为了二叉树本身。第十三天的递归思维是后面图论、动态规划甚至回溯算法的基石。树的DFS学会了,图的DFS就是加了一个visited数组;树的层序遍历学会了,图的BFS就是多写一个visited集合。动态规划里的"状态转移"本质上也和"树的递归合并结果"是同一个思维模式。

所以我的判断是:在二叉树这里多花时间是值得的。哪怕进度比别人慢几天,只要把每一道题都吃透、把递归框架内化成自己的肌肉记忆,后面会越走越轻松。反过来,如果这周草草带过,等到学图和DP的时候再回来补二叉树,付出的时间成本反而更高。

最后再分享一个训练营里流传的练习方法:晚上睡前拿纸笔手写一棵随机树,然后默写三种遍历和最大深度的代码,写完再睡觉。坚持一个星期,你再看二叉树题目,心态会完全不一样。我用这个方法熬过了最难的那两天,后面遇到再新的二叉树题,也只要往递归框架里套就行了。训练营第十三天只是个开始,把这个地基打牢,后面的算法之路会稳得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 10:39:51

AI Agent 实战:从环境配置到任务拆解,避开那些坑

1. 从“玩具”到“工位”&#xff1a;我对 AI Agent 的认知转折点 刚接触 AI Agent 那会儿&#xff0c;我跟大多数人一样&#xff0c;觉得这东西就是个“能自己调工具的聊天机器人”。你给它一句话&#xff0c;它帮你查天气、搜网页、写段代码&#xff0c;看起来挺酷&#xff0…

作者头像 李华
网站建设 2026/10/8 10:39:37

2026 扁平足足底压力分布异常诊断原理是什么?足压测力台设备厂家

【引言】2026 年,步态分析技术已深度嵌入康复门诊、运动训练与鞋类研发三大场景。扁平足的评估方式正经历一场从"经验目测"到"数据驱动"的转型——足底压力测量设备将不可见的力学变化转化为可量化、可追溯的客观指标。本文围绕广州欧迈志传感科技有限公司…

作者头像 李华
网站建设 2026/10/8 10:39:30

凸优化核心概念与工程实践:从凸集、凸函数到KKT条件与CVXPY应用

凸优化这几年算是被机器学习带火的一个方向。表面上它是数学课里的老古董&#xff0c;但实际上做模型训练、资源调度、信号处理、工程控制的人都绕不开它。我自己刚开始接触凸优化的时候&#xff0c;第一感受就是"概念太多太绕"&#xff1a;凸集、凸函数、强凸、次梯…

作者头像 李华
网站建设 2026/10/8 10:37:25

PHP setcookie() 函数

前言 setcookie() 的作用是让 PHP 在响应里追加一个 Set-Cookie 响应头&#xff0c;从而把一小段数据交给浏览器保存下来&#xff0c;并在后续请求里自动带回来。听起来很简单&#xff0c;但它牵扯到 HTTP 协议本身的一条硬性限制&#xff1a;Cookie 属于响应头的一部分&#x…

作者头像 李华
网站建设 2026/10/8 10:36:33

30分钟零代码搭建AI工作流:DeepSeek Harness插件实战指南

1. 为什么我花30分钟搭了这条AI工作流 先说结论&#xff1a;DeepSeek Harness v0.2 这个桌面端&#xff0c;我前后折腾了大概半小时&#xff0c;从下载安装到跑通一条能自动抓网页、整理成 Markdown、再归档到本地目录的工作流。整个过程没有写一行代码&#xff0c;全靠插件拼装…

作者头像 李华
网站建设 2026/10/8 10:36:08

研发总监AI实战:DFMEA、DOE与六西格玛的五大应用场景

1. 研发总监的AI能力补位逻辑 1.1 为什么研发总监需要亲自下场用AI 我带研发团队快八年了&#xff0c;从消费电子到工业设备都趟过。说实话&#xff0c;研发总监这个位置最尴尬的地方在于&#xff1a;你离一线技术越来越远&#xff0c;但离业务决策越来越近。图纸评审你要签字…

作者头像 李华