引言
欢迎来到本系列的第六篇!告别了线性的数组和链表,今天我们将踏入一个更广阔、更具层次感的数据结构世界——树 (Tree),并从它的最基本形态——二叉树 (Binary Tree) 开始探索。
二叉树是面试中名副-其实的“明星”,其相关问题层出不穷。为了稳固地开启我们的二叉树之旅,我们将从最经典、最基础的问题 LeetCode 104. 二叉树的最大深度 入手。
第一次看到这个问题,你可能会尝试通过数学公式,从节点总数来推算深度。但你会很快发现,这个方法只适用于“完美”的树。那么,对于任意形状的二叉树,我们该如何求解呢?
本文将带你一起:
-
分析“数学公式法”的局限性,理解为何它不适用于所有二叉树。
-
深入理解递归思想如何与树的“分形”结构完美契合。
-
掌握求解树的深度的“黄金法则”,并写出极其简洁优雅的递归代码。
一、题目与前置知识
-
题目编号:104. 二叉树的最大深度
-
题目难度:简单
-
题目描述:
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
-
示例:
-
输入: root = [3,9,20,null,null,15,7] (对应下图)
-
输出: 3
3 / \ 9 20 / \ 15 7
-
前置知识:二叉树节点定义
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
二、我的思考:直觉思路的“陷阱”
看到“深度”,我的第一反应是它和“节点总数”之间应该有某种数学关系。对于一个每层都铺满的“完美”二叉树(满二叉树),其节点数和深度的关系是明确的。例如,深度为 3 的满二叉树有 2^3 - 1 = 7 个节点。
于是,我最初的想法是:能不能通过总节点数,反推出树的深度?
这个思路的困境:
这个方法对于形态规整的树是有效的,但对于任意形状的二叉树则完全行不通。思考一个极端的“链式”二叉树:
1
\
2
\
3
这棵树只有 3 个节点,如果按照完全二叉树的公式计算,深度应该是 2。但它的实际最大深度却是 3!
结论:我们无法依赖一个简单的数学公式。我们需要一种能够适应树的任意结构的方法。这正是递归大显身手的地方。
三、豁然开朗:递归的“分解”艺术
树形问题天生就适合用递归来解决,因为树本身就是一个递归定义的数据结构:一棵树,是由一个根节点和它的左、右两棵子树组成的。
我们可以利用这个特性,将一个“求整棵树的深度”的大问题,分解成“求其子树深度”的小问题。
求解最大深度的“黄金法则”:
一棵树的最大深度 = 1 (根节点本身占一层) + 其左右子树中,深度较大的那个的深度。
让我们用这个法则来手动求解示例中的树:
3
/ \
9 20
/ \
15 7
-
求 maxDepth(根节点 3) 的深度:
-
根据法则,它等于 1 + max( maxDepth(左子树 9), maxDepth(右子树 20) )。
-
-
分解问题,先求子问题:
-
求 maxDepth(左子树 9):
-
节点 9 是叶子节点,它的左右子树都是空 (null)。
-
maxDepth(9) = 1 + max( maxDepth(null), maxDepth(null) )
-
我们定义空树的深度为 0。
-
所以,maxDepth(9) = 1 + max(0, 0) = 1。
-
-
求 maxDepth(右子树 20):
-
maxDepth(20) = 1 + max( maxDepth(15), maxDepth(7) )。
-
maxDepth(15) 和 maxDepth(7) 都是叶子节点,它们的深度计算出来也都是 1。
-
所以,maxDepth(20) = 1 + max(1, 1) = 2。
-
-
-
汇总结果:
-
现在我们有了子问题的答案:左子树深度为 1,右子树深度为 2。
-
代入第一步的公式:maxDepth(3) = 1 + max(1, 2) = 3。
-
我们得到了正确答案!这个“分解-求解-汇总”的过程,就是递归的精髓。
四、C++ 最优代码与详解
这个递归思路可以被直接、优雅地翻译成代码。
完整代码
#include <algorithm> // 为了使用 std::max
class Solution {
public:
int maxDepth(TreeNode* root) {
// 1.
if (root == nullptr) {
return 0;
}
// 2.
int left_depth = maxDepth(root->left);
int right_depth = maxDepth(root->right);
// 3.
return 1 + std::max(left_depth, right_depth);
}
};
代码逐点解释
-
if (root == nullptr)
这是递归的终止条件 (Base Case)。当递归到空节点时(比如一个叶子节点的子节点),我们知道空树的深度为 0,直接返回。这是递归能够停止并开始“回归”的关键。 -
int left_depth = maxDepth(root->left); int right_depth = maxDepth(root->right);
这是问题的分解。我们不关心左右子树内部有多复杂,我们只是信任地调用 maxDepth 函数自身去解决这两个规模更小的子问题。函数会一路递归下去,直到遇到终止条件,然后逐层返回子问题的解。 -
return 1 + std::max(left_depth, right_depth);
这是结果的汇总。当两个子问题 maxDepth(root->left) 和 maxDepth(root->right) 都返回了它们的答案后,当前层就根据我们的“黄金法则”来计算自己的深度:1 (当前层) + 左右子深度中的较大值,然后将这个结果返回给它的上一层调用。
五、总结与收获
-
复杂度分析:我们访问了树中的每一个节点一次,因此时间复杂度为 O(n),其中 n 是节点的数量。递归调用栈的深度在最坏情况下(链式树)等于树的高度,因此空间复杂度为 O(h),其中 h 是树的高度。
-
核心思想:递归是解决树形问题的首选“瑞士军刀”。通过定义好终止条件和递归关系,我们可以将复杂的大问题分解为简单的子问题来求解。
-
关键技巧:求解树的高度/深度问题的核心公式是 1 + max(左子树高度, 右子树高度)。这个模式在许多其他树形问题中也会反复出现。