从零开始写算法——二叉树篇3:对称二叉树 + 二叉树直径

二叉树是数据结构中递归思想的最佳演练场。很多同学在刷题时,往往看答案觉得很简单,自己写却无从下手。

其实,二叉树的递归无非就两种套路:

  1. 怎么传下去?(父节点把要求传给子节点,如判断对称)

  2. 怎么收上来?(子节点把结果汇报给父节点,如计算直径)

今天我们通过两道经典题目(LeetCode 101 和 543),来彻底搞懂这两个套路。


01. 对称二叉树 (Symmetric Tree)

题目解析

我们要判断一棵树是不是轴对称的。这就好比我们在树的中间放一面镜子,左边的样子和右边的样子必须完全重合。

核心逻辑:镜像对比

很多初学者容易犯的错误是:直接判断 root->leftroot->right 是不是"完全相同"的树。 错! 对称不是"相同",而是"镜像"。

我们可以把这棵大树拆解成无数个小的"三角单元"。对于任意两个对称位置的节点 p(左边那个)和 q(右边那个),如果要满足对称,必须同时满足三个条件:

  1. 值相等p 的值必须等于 q 的值。

  2. 外侧对齐p 的左孩子(最外侧)必须等于 q 的右孩子(最外侧)。

  3. 内侧对齐p 的右孩子(靠近中间)必须等于 q 的左孩子(靠近中间)。

代码深度拆解

C++代码实现:

cpp 复制代码
class Solution {
public:
    bool isSymmetric(TreeNode* root) {
        // 根节点的左子树和右子树进行镜像对比
        return isSameTree(root->left, root->right);
    }
    
    // 这里虽然函数名叫 isSameTree,但实际逻辑是 checkMirror
    // 作用:判断 p 和 q 两棵树是否互为镜像
    bool isSameTree(TreeNode* p, TreeNode* q) {
        // 1. 递归终止条件:处理空节点的情况
        // 如果两个都为空,说明到底了且匹配,返回 true
        // 如果只有一个为空,说明不对称(结构不同),返回 false
        if (p == nullptr || q == nullptr) return q == p;
        
        // 2. 核心递归逻辑
        // 条件A: 当前节点值必须一样
        // 条件B: p的左边 VS q的右边 (外侧互相对比)
        // 条件C: p的右边 VS q的左边 (内侧互相对比)
        return p->val == q->val 
            && isSameTree(p->left, q->right) 
            && isSameTree(p->right, q->left);
    }
};

为什么这么写? 代码中最精髓的一句是 isSameTree(p->left, q->right) && isSameTree(p->right, q->left)。这正是体现了**"镜像"**的定义。如果你写成了 p->left, q->left,那就是判断两棵树是否完全一样(平移),而不是对称了。

复杂度分析

  • 时间复杂度O(N)。 我们需要遍历树中的每一个节点一次。

  • 空间复杂度O(H),其中 H 是树的高度。 这里的空间消耗主要来自递归调用的栈空间。在最坏情况下(树退化成链表),高度为 N;最好情况下(完全二叉树),高度为 logN。


02. 二叉树的直径 (Diameter of Binary Tree)

题目解析

这道题要求的是二叉树中任意两个节点间的最长路径长度。 注意一个大坑 :这条最长路径不一定经过根节点。它可能完全存在于左子树内部,也可能在右子树内部。

核心逻辑:后序遍历 + 全局打擂台

既然路径可能出现在任何地方,但所有的路径都有一个共同点:它一定有一个"最高点"(转折点) 。 对于任何一个节点,穿过该节点 的最长路径 = 左子树的最大深度 + 右子树的数据深度

所以我们的策略是:

  1. 自底向上(后序遍历):先算出子树的深度,再汇报给父节点。

  2. 全局打擂 :在计算深度的过程中,顺便计算"经过当前节点的最长路径",并用一个全局变量 ans 记录历史最大值。

代码深度拆解

C++代码实现:

cpp 复制代码
class Solution {
    // 全局变量维护最大直径
    // 因为最长路径可能不经过根节点,所以需要在遍历过程中实时更新最大值
    int ans = 0;
    
    // 这个函数的定义非常关键:
    // 它的作用是:返回以 root 为根的树的【最大深度】
    // 它的副作用是:更新经过 root 的【最大直径】
    int dfs(TreeNode* root) {
        // 1. 终止条件:空节点深度为 0
        if (root == nullptr) return 0;
        
        // 2. 递归获取左右子树的深度(自底向上)
        int left = dfs(root->left);
        int right = dfs(root->right);
        
        // 3. 【核心逻辑之一:更新答案】
        // 经过当前节点的最长路径 = 左臂长 + 右臂长
        // 这是一条"穿过"当前节点的路径,类似倒"V"字型
        ans = max(ans, left + right);
        
        // 4. 【核心逻辑之二:返回深度】
        // 当前节点要汇报给父节点的是:我这条分支有多长?
        // 我只能选左边或右边更长的一条路继续往上延伸,不能两条都选(那样就分叉了)
        // 所以返回 max(left, right) + 1 (加1是加上当前节点自己)
        return max(left, right) + 1;
    }
    
public:
    int diameterOfBinaryTree(TreeNode* root) {
        dfs(root);
        return ans;
    }
};

为什么这么写? 很多同学搞混淆的地方在于:为什么更新 ans 用加法,而返回值用 max

  • 更新 ans :我们在找直径 (路径长度)。一条路径穿过当前节点,是把左腿和右腿连起来,所以是 left + right

  • 返回值 :我们在算深度 (高度)。对于父节点来说,当前节点只是它的一条"腿"。一条路径不能分叉,只能选最长的那条腿往上走,所以是 max(left, right) + 1

复杂度分析

  • 时间复杂度O(N)。 每个节点被访问一次,计算深度是 O(1) 的操作。

  • 空间复杂度O(H)。 同样取决于递归栈的深度。最坏情况 O(N),平均情况 O(logN)。


总结

这两道题虽然都是递归,但方向截然不同:

  1. 对称二叉树带着任务往下跑:拿着左边的节点去和右边匹配,这是一种自顶向下的逻辑。

  2. 二叉树直径带着结果往上报:子节点算好了深度,汇报给父节点,父节点顺便计算一下经过自己的最长路径,这是一种自底向上的逻辑。

掌握了这两种思维,你对二叉树的递归理解就上了一个台阶!

相关推荐
智者知已应修善业15 分钟前
【51单片机8位数码管同时倒计时从9999】2024-1-25
c++·经验分享·笔记·算法·51单片机
洛水水18 分钟前
【力扣100题】86.柱状图中最大的矩形
算法·leetcode·职场和发展
渡之25 分钟前
GRiM-Net 深度解析 | 无人机 GNSS 拒止场景下两阶段跨视角视觉定位框架
深度学习·算法·动态规划·无人机
测试仪器廖生1359025638544 分钟前
罗德与施瓦茨 FSP13频谱分析仪FSP30
网络·人工智能·算法
happymaker06261 小时前
LeetCodeHot100——560.和为K的子数组
算法
dtq04241 小时前
C语言刷题数组5,6(求平均值,求最大值)
c语言·数据结构·算法
郭梧悠1 小时前
Hash算法入门Hash冲突解决方案
算法·哈希算法
洛水水2 小时前
【力扣100题】81.寻找两个正序数组的中位数
数据结构·算法·leetcode
happymaker06262 小时前
LeetCodeHot100——155.最小栈
算法
洛水水3 小时前
【力扣100题】85.每日温度
算法·leetcode·职场和发展