尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

二叉树核心考点全解:特殊结构、存储选择与遍历算法实战

二叉树核心考点全解:特殊结构、存储选择与遍历算法实战 二叉树的题做过不少但真到了面试或者笔试现场很多人还是容易栽在细节上。不是不会遍历是一碰到特殊结构、存储方式、递归转迭代这些问题就乱了阵脚。这篇文章不整虚的直接把二叉树这块的“地基”给你夯实特殊结构有哪些坑、双重存储怎么选、遍历算法怎么从递归玩到迭代再玩到层次全部串起来讲透配合能直接上手的代码和踩坑记录看完你就能在二叉树这个题型上彻底站稳。1. 为什么二叉树是所有数据结构的“分水岭”1.1 从链表到二叉树的思维跃迁线性数据结构比如数组、链表、栈、队列它们的共同点是“一个元素最多只有一个直接后继”操作时顺着一条线走到底就行。到了二叉树这里规则变了一个节点最多有两个分支左孩子、右孩子数据之间的关系从“一对一”变成了“一对多”。别小看这个变化它带来的第一个冲击就是你的遍历思维必须从“单线”切换成“分叉”。以前写链表遍历一个 while 循环从头走到尾现在写二叉树遍历你面对的是“先走左还是先走右”“走完左要不要回来走右”这样的选择。递归之所以在二叉树里这么好用本质上就是因为树形结构天然自带“递归子问题”的属性——每棵子树的处理方式和整棵树完全一样。第二个冲击是“回溯”。在链表里你几乎不需要回溯但在二叉树里无论前序、中序、后序你都必须从叶子节点一层层往回退才能继续访问另一个分支。这个“退回去”的动作正是递归调用栈在做的事也正是你把递归改成迭代时需要用栈来手动模拟的核心逻辑。第三个冲击是“形态多样化”。链表只有一种形状二叉树却有满二叉树、完全二叉树、平衡二叉树、搜索二叉树、线索二叉树等一堆变体。每个变体都有自己的约束条件和适用场景这就是为什么必须单独花时间把“特殊结构”梳理清楚。1.2 二叉树在真实世界中的映射很多人学二叉树觉得抽象是因为没把它和真实系统对上号。我这里举几个最常见的落地场景。文件系统目录每个目录下面有子目录和文件子目录下面又有子目录这就是一棵多叉树。如果只允许两个分支它就退化成二叉树模型。表达式求值(a b) * c可以用二叉树表示操作符在根节点左右孩子是操作数。后序遍历这棵树输出的就是后缀表达式正好对应计算器求值的顺序。数据库索引B 树是多路搜索树但它的基础思想就是从二叉搜索树演化来的。搞懂了二叉搜索树“左小右大”的规则再看 B 树的分裂和查找思路是相通的。哈夫曼编码根据字符出现频率构建一棵二叉树哈夫曼树出现频率越高的字符离根节点越近编码越短。这就是压缩软件能把文件变小的原理。路由表与 Trie 树网络路由的最长前缀匹配本质上也依赖树形结构的分支查找逻辑。把这些场景记住再回去看二叉树的各种操作你会发现它们不是孤立的算法题而是一套真实可用的组织数据的方式。1.3 学完二叉树你应该拥有的“能力清单”这篇文章讲完你至少应该具备以下能力能准确区分满二叉树、完全二叉树、平衡二叉树、搜索二叉树、线索二叉树并能说出它们各自的特点和判定标准。能解释顺序存储和链式存储的各自优缺点并根据场景做出选择。能手写前序、中序、后序、层次遍历的递归和迭代版本能讲清楚“访问根节点”这件事在三种遍历中的顺序差异。能用宽度优先搜索BFS和深度优先搜索DFS两种思路解决二叉树相关的常见算法题比如求深度、求最近公共祖先、判断对称性、层序遍历等。能把“中序 前序”或“中序 后序”的遍历序列还原成一棵二叉树。这些能力不是靠背代码得到的是靠理解“为什么”得到的。下面就从特殊结构开始一块一块给你拆开。2. 特殊二叉树结构满二叉树、完全二叉树、平衡二叉树、搜索二叉树、线索二叉树2.1 满二叉树与完全二叉树名字像考点不同先看满二叉树。定义很简单除了叶子节点外每个节点都有左右两个孩子并且所有叶子节点都在同一层。也就是说一棵高度为 h 的满二叉树节点总数是 2^h - 1如果根节点高度记为 1。这个公式经常被用来出题比如问“高度为 4 的满二叉树有多少个节点”答案就是 2^4 - 1 15。完全二叉树和满二叉树很容易搞混但实际上完全不同。完全二叉树的定义是除了最后一层外其他每一层都必须满节点且最后一层的节点必须连续集中在左侧不能出现“左边空了右边还有”的情况。换句话说完全二叉树不要求每个节点都有两个孩子但它必须“从左往右依次排满”。判断一棵树是不是完全二叉树有个很实用的层序遍历技巧按层遍历整棵树一旦遇到某个节点没有左孩子或没有右孩子那么它后面的所有节点都必须没有孩子。说白了就是“缺口只能出现在最后一层的最右侧”。这个判断方法在面试中经常被问到可以顺手写一个层序遍历版本。满二叉树一定是完全二叉树反过来则不一定成立。比如一棵高度为 3 的树最后一层只有两个节点且都靠在左边它是完全二叉树但不是满二叉树。这个逻辑关系必须牢牢记住。2.2 平衡二叉树与搜索二叉树两个高频“限定词”平衡二叉树AVL 树的名字来源于发明者 Adelson-Velsky 和 Landis的核心约束是任意节点的左右子树高度差绝对值不超过 1。为什么要引入平衡因为如果一棵二叉搜索树一直往单侧插入数据它就会退化成一条链表查找复杂度从 O(log n) 变成 O(n)。加上“平衡”约束后树的高度始终保持在 log n 级别查找效率就稳定了。搜索二叉树也叫二叉排序树、二叉查找树BST的核心约束是任意节点的左子树所有值都小于该节点值右子树所有值都大于该节点值。注意是“左子树所有值”不只是左孩子。这个“所有”很重要因为你在判断一棵树是否合法 BST 时必须把整棵子树的值都验证到不能只比较当前节点和它的直接孩子。题目里经常出现“判断一棵树是不是合法的二叉搜索树”很多人一上来就只判断左孩子 根 右孩子结果忽略了“左子树里的最大值也必须小于根节点”这个约束。标准的解法是给每个节点传一个取值范围 (min, max)递归时左子树收紧上界右子树收紧下界。平衡二叉树和搜索二叉树经常组合出题比如“判断一棵树是否是平衡二叉搜索树”就是把两个条件都写上。还有一种高频变体给你一个有序数组要求构建一棵高度平衡的二叉搜索树。这题其实不难思路就是每次取数组中间元素作为根左边子数组构建左子树右边子数组构建右子树用递归很容易写出来。2.3 线索二叉树让遍历不再依赖递归和栈线索二叉树是很多教材里讲、但很多同学不太理解的结构。它的出现是为了解决一个效率问题链式存储的二叉树在遍历时如果你不用递归就必须借助栈或者队列等辅助空间。而二叉树里有很多空的指针域叶子节点的左右指针都是空的能不能把这些空指针利用起来直接指向遍历序列中的前驱和后继节点答案就是线索化。规则如下如果某个节点没有左孩子就把它的左指针指向“前驱节点”。如果某个节点没有右孩子就把它的右指针指向“后继节点”。为了区分指针到底是指向真实的孩子还是指向前驱/后继每个节点还需要增加两个布尔标记位ltag 和 rtag0 表示指向孩子1 表示指向前驱或后继。线索化的过程本质上是做一次遍历在遍历过程中记录“上一个访问的节点”然后让当前节点去“补线”。中序线索二叉树是最常见的因为中序序列本身把一棵树线性化了线索化之后你就能像遍历链表一样沿着后继指针走完整棵树不需要递归也不需要栈。这个结构在真实工程里用得不算多但算法题和考试题里会考尤其是结合“给定中序线索二叉树求某节点的前驱/后继”来出题。我建议你至少能手写一遍中序线索化的递归代码逻辑清楚了考试时随便怎么变形都能应对。2.4 堆隐藏在数组里的完全二叉树堆是一种特殊的完全二叉树它有两种形态大顶堆每个父节点都大于等于孩子节点和小顶堆每个父节点都小于等于孩子节点。它的特殊之处在于存储方式——直接用数组存储不需要节点指针。堆的数组存储规律非常优美假设根节点放在下标 1 的位置有些实现从 0 开始但下标计算要做相应调整节点 i 的左孩子下标2 * i节点 i 的右孩子下标2 * i 1节点 i 的父节点下标i / 2向下取整堆排序、优先队列、Top K 问题全部依赖这个结构。很多人学堆排序觉得难是因为没有把“完全二叉树”和“数组”这两个视角打通。当你把数组想象成一棵完全二叉树再去做“下沉”和“上浮”操作逻辑就会清晰很多。面试里经常考“从数据流中找中位数”这类题解法就是用两个堆一个大顶堆存较小的前半部分一个小顶堆存较大的后半部分保证两个堆的大小差不超过 1。这就是堆这个特殊二叉树的经典应用。3. 双重存储结构顺序存储和链式存储的取舍3.1 顺序存储如何用一维数组铺平一棵树顺序存储的核心思想是利用完全二叉树的编号规律把树的节点存进数组通过下标之间的数学关系找到父子和兄弟节点。上面提到过根节点放下标 1某个节点下标为 i 时左孩子是 2i右孩子是 2i1父节点是 i/2。这套规律只有当树是“基本满”的形态时才不会浪费空间。如果你存一棵单链状的退化二叉树每个节点只有一个孩子用数组存就会有一半以上的位置是空的空间利用率极低。顺序存储的优点是什么呢不需要存储指针空间利用率在“满树”情况下非常高。可以通过下标随机访问任意节点时间复杂度 O(1)。CPU 缓存友好数组是连续内存遍历时命中率高。缺点也很明显插入和删除时需要移动大量元素时间复杂度高。对树形不规则的二叉树会产生大量空洞空间浪费严重。动态扩容不方便需要预先估计树的规模。适用场景就是堆这种“天生完全且基本不变”的结构。你去看优先队列的底层实现底层就是一个动态扩容的数组辅助上浮和下沉操作完全不用指针。3.2 链式存储三个指针域的节点设计链式存储的节点设计非常直观一个数据域加两个指针域typedef struct TreeNode { int val; struct TreeNode *left; struct TreeNode *right; } TreeNode;左右指针分别指向左孩子和右孩子没有孩子就为 NULL。这种结构对任意形态的二叉树都适用插入和删除节点时只需要调整指针不需要移动数据灵活性极高。它的缺点是每个节点都要额外负担两个指针的内存开销在极端情况下比如一颗斜树指针域的利用率其实很低。不过对于算法题来说链式存储永远是你的默认选择因为题目给的树基本都是链式结构操作起来也最自然。还有一种三叉链表的设计在左右指针之外再加一个 parent 指针指向父节点。这在需要“从子节点访问父节点”的场景下很有用比如找最近公共祖先时可以先从一个节点沿 parent 指针回溯到根记录路径再从另一个节点回溯比对。虽然实际做题时我们通常用递归来解决但在工程实现里三叉链表能省掉很多重复遍历。3.3 顺序存储 vs 链式存储到底怎么选我把两种存储方式的对比整理成一张表方便你做题和面试时快速组织语言对比维度顺序存储链式存储存储单位数组连续内存节点 指针非连续内存空间利用率完全二叉树极高斜树极低稳定但需额外指针空间随机访问O(1) 按下标访问必须从根节点遍历找插入删除可能需要移动大量元素调整指针即可O(1) 时间修改局部适用场景堆、完全二叉树、静态树一般二叉树、动态变化的树实现难度简单但下标计算易错需要管理指针注意空指针实战里动态构建一颗二叉树几乎都用链式存储因为你不知道树的形状和规模链式结构随插随建非常方便。只有在明确知道是一棵完全二叉树比如堆排序场景时才优先考虑顺序存储。3.4 手写链式二叉树的创建与基本操作算法题里最常见的创建方式是根据“空节点标记数组”来建树比如输入一个数组[3, 9, 20, None, None, 15, 7]表示按层填充一棵树None 表示空节点。用队列做层序构建是最直观的from collections import deque def build_tree(values): if not values or values[0] is None: return None root TreeNode(values[0]) queue deque([root]) i 1 while queue and i len(values): node queue.popleft() if i len(values) and values[i] is not None: node.left TreeNode(values[i]) queue.append(node.left) i 1 if i len(values) and values[i] is not None: node.right TreeNode(values[i]) queue.append(node.right) i 1 return root这段代码的关键在于每处理一个节点就从数组里取两个值作为它的左右孩子不管是不是 None 都算一个位置所以 i 每次要前进两步。列表里的 None 只是占位符不创建节点但数组下标仍然要跳过。你手动跑一遍[1, 2, 3, None, 4]观察下标的变化就能理解它是如何按层把数组“铺”到树上的。4. 遍历算法前序、中序、后序与层次遍历全拆解4.1 为什么遍历是二叉树的核心操作二叉树的所有算法题无论是求深度、判断对称、查找路径、重建二叉树本质都是在做一件事按某种顺序访问树中的所有节点。区别只在于访问的顺序不同以及你在这棵树上收集什么信息。深度优先搜索DFS对应的是前序、中序、后序遍历它们都是深度优先的遍历方式只是根节点的访问时机不同。广度优先搜索BFS对应的是层次遍历逐层访问。这两个大类覆盖了几乎所有二叉树问题的解法模板。前序根节点 - 左子树 - 右子树 中序左子树 - 根节点 - 右子树 后序左子树 - 右子树 - 根节点这里最容易混淆的点是“左子树在根之前还是之后”的顺序。我建议你不背口诀而是手画一棵树实际走一遍。比如一棵简单树1 / \ 2 3 / \ 4 5前序结果1, 2, 4, 5, 3中序结果4, 2, 5, 1, 3后序结果4, 5, 2, 3, 1自己按“访问根”的位置标一遍比背十遍口诀都有用。4.2 递归遍历三行代码背后的顺序原理递归版的前序、中序、后序遍历代码量几乎一模一样就是三行语句的顺序互换def preorder(root): if not root: return print(root.val) # 前序先访问根 preorder(root.left) preorder(root.right) def inorder(root): if not root: return inorder(root.left) print(root.val) # 中序左根右 inorder(root.right) def postorder(root): if not root: return postorder(root.left) postorder(root.right) print(root.val) # 后序左右根这几段代码好写但要吃透背后的“递归栈”变化过程。以中序遍历为例当你处理节点 1 时不是先打印 1而是先递归进入左子树 2再进入 2 的左子树 44 没有左孩子所以打印 4返回 2打印 2再进入 2 的右子树 5……这个过程就是不断“向左到底然后回头访问根再向右”。理解这个执行顺序后你就能解释为什么中序遍历一棵二叉搜索树得到的结果是升序序列——因为 BST 的特性就是左小右大中序正好按“左根右”的顺序把所有节点从小到大访问了一遍。4.3 迭代遍历用栈模拟递归调用过程面试时除了递归还经常要求你写迭代版本。原因有两个一是递归深度过大容易导致栈溢出严格说程序栈溢出而不是 C STL 的栈结构二是迭代版本能体现你对递归本质的理解。前序迭代最容易实现。用一个栈存储待访问节点先把根节点入栈每次弹出一个节点先打印或收集然后先把右孩子入栈再把左孩子入栈。为什么先右后左因为栈是后进先出左孩子后入栈就先弹出从而保证“根左右”的访问顺序。def preorder_iter(root): if not root: return [] stack [root] res [] while stack: node stack.pop() res.append(node.val) if node.right: stack.append(node.right) if node.left: stack.append(node.left) return res后序迭代比较特殊有一个技巧前序是“根左右”后序是“左右根”。如果你把前序改成“根右左”访问也就是入栈时先左后右然后反转结果就得到了“左右根”。代码非常简单def postorder_iter(root): if not root: return [] stack [root] res [] while stack: node stack.pop() res.append(node.val) if node.left: stack.append(node.left) if node.right: stack.append(node.right) return res[::-1]中序迭代是三者中最难的。它的核心逻辑是一路向左走到底把沿途节点全部压入栈中然后弹出一个节点访问它再转向它的右子树继续重复上述过程。def inorder_iter(root): stack [] res [] cur root while cur or stack: while cur: stack.append(cur) cur cur.left cur stack.pop() res.append(cur.val) cur cur.right return res这个写法建议你亲手模拟一遍起初 cur 是根节点不断往左压栈直到 None出栈一个节点这就相当于访问了“中序序列中的下一个节点”然后 cur 跳到右子树继续向左压栈。整个过程完美复刻了递归中序遍历的执行顺序。4.4 层次遍历队列配合逐层输出层次遍历广度优先遍历BFS和深度优先遍历的思路完全不同。它是按层从上到下、从左到右访问节点天然适合用队列实现from collections import deque def level_order(root): if not root: return [] queue deque([root]) res [] while queue: level_size len(queue) # 记录当前层节点数 level_vals [] for _ in range(level_size): # 只弹出当前层的节点 node queue.popleft() level_vals.append(node.val) if node.left: queue.append(node.left) if node.right: queue.append(node.right) res.append(level_vals) return res关键在于level_size len(queue)。如果不先记录当前层的节点数直接在 while 循环里用queue的长度来判断等子节点入队后长度就变了你无法区分哪些节点在同一层。层次遍历的变体题特别多自底向上的层序遍历得到结果后反转res。锯齿形之字形层序遍历奇数层从左到右偶数层从右到左可以在压入level_vals时根据层号决定正序还是倒序。求二叉树的右视图每一层只取最后一个节点的值。求最大宽度可以给每个节点编号用层序遍历计算最左边和最右边节点的编号差。4.5 从遍历到解题重建二叉树与序列化遍历不只是用来输出的它还能反推树的结构。最经典的题型是已知前序 中序遍历序列重建二叉树。原理是前序序列的第一个元素是根节点。在中序序列中找到根节点的位置左边是左子树的中序序列右边是右子树的中序序列。根据左子树中序序列的长度可以在前序序列中划分出左子树和右子树的前序序列。递归重建左右子树。def build_tree_from_pre_in(preorder, inorder): if not preorder or not inorder: return None root_val preorder[0] root TreeNode(root_val) idx inorder.index(root_val) # 找到根在中序中的位置 left_in inorder[:idx] right_in inorder[idx1:] left_len len(left_in) left_pre preorder[1:1left_len] right_pre preorder[1left_len:] root.left build_tree_from_pre_in(left_pre, left_in) root.right build_tree_from_pre_in(right_pre, right_in) return root这个题的难点不是递归本身而是“划分区间”时下标的计算。建议你手动画一下例子比如preorder [3, 9, 20, 15, 7]inorder [9, 3, 15, 20, 7]把每一步的左右子数组写出来很快就清楚了。强调一句只有前序 中序或后序 中序才能唯一确定二叉树前序 后序无法唯一确定因为无法区分左右子树。5. 实战中必须避开的坑与高频题型5.1 递归改迭代时最容易踩的三个坑第一个坑是“什么时候处理根节点”。前序迭代是弹出即处理中序迭代是出栈即处理后序迭代是反转前序结果。很多人一着急就把前序的逻辑套到中序里结果一团糟。建议你记住一件事永远是“栈顶元素决定了下一个被访问的节点”想清楚当前节点是“被压入”还是“被弹出”顺序就顺了。第二个坑是空指针。尤其在中序迭代里while cur or stack这个条件很容易漏掉cur为空但栈非空的情况。写成while cur and stack会导致树处理一半就退出。建议每次写完循环条件后手动把一棵只有一个左子树的树套进去检查。第三个坑是递归深度。在实际的工程项目中如果树的深度很大比如 10 万层递归版本的程序会直接崩溃。所以“递归改迭代”并不是面试官的刁难而是一个真实工程问题。如果你用系统栈会溢出就必须换显式栈。这个意识一定要有。5.2 求二叉树深度的边界条件与两种解法二叉树深度高度的定义有两种理解方式一种是根节点深度为 0一种是深度为 1。刷题平台通常默认根节点深度为 1即空树深度为 0只有一个根节点的树深度为 1。做题前最好先确认清楚否则返回值差 1会导致结果判断出错。递归写法非常简洁def max_depth(root): if not root: return 0 return max(max_depth(root.left), max_depth(root.right)) 1这个递归的终止条件是“空节点返回 0”每往上一层就加 1。另一种思路是层序遍历def max_depth_bfs(root): if not root: return 0 queue deque([root]) depth 0 while queue: level_size len(queue) for _ in range(level_size): node queue.popleft() if node.left: queue.append(node.left) if node.right: queue.append(node.right) depth 1 return depth层序遍历版本每处理完一层深度加 1。两种方法复杂度都是 O(n)但递归写起来更短层序更符合直觉也更容易扩展成“每层计数”的题目。后面我会单独说层序遍历。最小深度就不一样了很多人直接抄最大深度的写法结果错了。最小深度指的是从根节点到最近叶子节点的最短路径上的节点数。注意“叶子节点”这个词——如果根节点只有右子树没有左子树那最小深度不是 1因为根节点本身不是叶子节点。正确做法是def min_depth(root): if not root: return 0 if not root.left: return min_depth(root.right) 1 if not root.right: return min_depth(root.left) 1 return min(min_depth(root.left), min_depth(root.right)) 1或者直接用层序遍历遇到第一个左右孩子都为空的节点返回当前深度这个思路最不容易踩坑。5.3 二叉树相关的面试高频题型整理我按频率和难度整理一份列表你可以把它当作刷题清单题型核心思路难度求二叉树深度/最小深度递归取最大/最小注意最小深度的叶子限制简单前/中/后序遍历迭代版显式栈模拟递归后序可反转简单层序遍历及变体队列 记录每层大小简单判断两棵树是否相同同步递归比较左右子树简单判断对称二叉树镜像递归比较左.右 与 右.左简单二叉搜索树的合法判断用 (min, max) 区间收紧验证中等最近公共祖先 LCA后序遍历 返回值标记中等路径总和系列递归记录当前路径和注意叶子判断中等中序前序/后序重建二叉树递归分割区间中等二叉树序列化与反序列化DFS 前序或 BFS 层序用标记补全空节点较难二叉树的最大路径和后序递归把子树最大贡献传回父节点较难其中最大路径和这道题很考验对后序遍历的理解。它的思路是对于每个节点把左子树的“最大贡献值”和右子树的“最大贡献值”加起来再加上自己的值得到以该节点为“拐点”的路径和但向上返回时只能返回“单边最大贡献 自身值”因为路径不能分叉。这个“向下返回单边全局更新双边”的思维模式是二叉树进阶题的常见套路值得多写几遍理解透。6. 二叉树学习的路径建议与心法6.1 一条循序渐进的自学路线第一阶段把遍历吃透。四种遍历的递归和迭代全都要能手写能画出递归栈的变化过程。这一关过不了后面全是空中楼阁。第二阶段用遍历解决简单问题。求深度、求节点个数、判断树相同、判断对称、求叶子节点数量——这些题不需要额外技巧就是遍历时顺手收集信息。第三阶段理解特殊结构。把搜索二叉树、平衡二叉树、完全二叉树、满二叉树、线索二叉树逐个吃透能说出它们的判定条件和典型应用。这一阶段也是考研和面试笔试的高频考察区。第四阶段挑战进阶题。重建二叉树、序列化、路径总和、最近公共祖先、最大路径和这些题目会逼你真正理解“递归返回值”和“全局变量收集结果”的区别。第五阶段结合其他数据结构。比如用哈希表优化重建二叉树时的中序定位用堆解决 Top K 问题用 BFS 解决多叉树结构N 叉树的层序遍历举一反三。6.2 写二叉树代码的几个好习惯第一先写终止条件。递归版所有问题都可以从“空节点怎么办”开始。确定if not root的返回值后再往下写会减少大量空指针错误。第二每写完一个递归函数立刻问三个问题返回值代表什么递推式是什么终止条件是什么这三个问题能回答清楚逻辑就是通的。第三注意边界值。求最小深度时叶子节点的判断重建二叉树时数组的切分位置层序遍历时当前层的节点数——这些是最容易出错的地方建议用铅笔在纸上画一棵小树模拟一遍。第四多用手动测试。不要依赖在线编译器的自测用例。自己构造几棵树比如空树、只有根节点的树、单侧树、完全二叉树、随机树把代码跑一遍观察输出是否符合预期。6.3 一套高效的代码调试思路二叉树代码出错后怎么排查我给你一个“人肉递归法”在递归函数里加打印语句打印当前访问节点的值和深度跑一遍测试用例观察打印顺序是否符合你的预期。比如中序遍历打印顺序应当是“左子树的所有节点 - 根 - 右子树的所有节点”。如果顺序是乱的说明递归调用顺序写错了。这个方法看似笨拙但对初学者特别有效。它能帮你把抽象的递归过程和具体的执行顺序对应起来。等写多了你会发现大约 80% 的递归错误都出在“返回值处理”和“对空节点的处理”上而不是调用顺序本身。我个人的经验是刷二叉树题目的时候不要追求一口气写对。先画图写出递归的三步走终止条件、递推式、返回值再上手敲代码。这个过程看着慢实际提升很快。还有一个心得是遇到不会的题先看别人的解法看懂后合上代码自己实现一遍第二天再重复一遍。这种“间隔重复”对形成条件反射非常有效。二叉树这块内容说多不多说少不少。把特殊结构、双重存储、四种遍历这三个维度彻底打通后面无论遇到排序、搜索、图论还是动态规划你都会发现它们多多少少和树有关。别贪快静下心来把一棵树的根、左、右想清楚很多问题都会迎刃而解。
返回列表