代码随想录算法训练营 Day13 | 二叉树 part03

110. 平衡二叉树

给定一个二叉树,判断它是否是 平衡二叉树

cpp 复制代码
// 递归法(后序遍历 + 剪枝)
class Solution {
public:
    // 返回值:如果平衡返回高度,不平衡返回 -1
    int height(TreeNode* root) {
        if (root == NULL) return 0;
        // 1. 递归左子树
        int left = height(root->left);
        if (left == -1) return -1; // 左子树不平衡,直接返回 -1 剪枝
        
        // 2. 递归右子树
        int right = height(root->right);
        if (right == -1) return -1; // 右子树不平衡,直接返回 -1 剪枝
        // 3. 判断当前节点是否平衡
        // 如果左右高度差 > 1,返回 -1 表示不平衡
        // 否则返回当前节点的高度
        return abs(left - right) > 1 ? -1 : max(left, right) + 1;
    }
    bool isBalanced(TreeNode* root) {
        // 只要返回值不是 -1,说明整棵树平衡
        return height(root) != -1;
    }
};

// 递归法(全局标志位)
class Solution {
public:
    bool flag; // 全局标志位
    int height(TreeNode* root) {
        if (root == NULL) return 0;
        // 递归计算高度
        int left = height(root->left);
        int right = height(root->right);
        // 【关键】检查高度差,如果不平衡,修改标志位
        if (abs(left - right) > 1) flag = false;
        // 返回正常的高度计算结果
        return max(left, right) + 1;
    }
    bool isBalanced(TreeNode* root) {
        flag = true; // 初始化为 true
        height(root);
        return flag;
    }
};

总结

1. 核心区别
方法 时间复杂度 特点 评价
方法1 (剪枝) O(N) 自底向上,发现不平衡立即返回 最优解,效率最高
方法2 (标志位) O(N) 自底向上,但发现不平衡不会立即停止计算 逻辑清晰,但不如方法1高效
2. 为什么方法1最好?
  • 剪枝机制:在方法1中,if (left == -1) return -1; 这一行非常关键。一旦左子树返回 -1,程序直接结束当前分支的递归,不再去计算右子树的高度。这避免了无意义的遍历。
  • 方法2 虽然复杂度也是 O(N),但即使 flag 变为 false,递归函数 height 依然会执行完毕直到栈回溯完毕。
3. 复杂度总结
  • 时间复杂度:
    • 方法1和方法2:每个节点只访问一次,O(N)。
    • 方法3:每个节点被多次访问(计算高度时重复遍历),O(N²)。
  • 空间复杂度:
    • 均取决于递归栈深度,最坏 O(N)。

257. 二叉树的所有路径

给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。

叶子节点 是指没有子节点的节点。

cpp 复制代码
class Solution {
public:
    vector<string> ans;
    // dfs 函数参数:当前节点,以及到达当前节点的路径字符串
    // 【关键点】:这里的 path 是"值传递",不是"引用传递"
    void dfs(TreeNode* root, string path) {
        // 1. 终止条件:遇到叶子节点(左右孩子都为空)
        if (root->left == NULL && root->right == NULL) {
            ans.push_back(path); // 将当前路径加入结果集
            return;
        }
        // 2. 递归逻辑
        // 如果左孩子存在,将 "左孩子值" 拼接到路径中,传入下一层
        if (root->left) {
            // path + "->" + ... 会创建一个新的临时 string 对象传给 dfs
            // 这意味着回到当前层时,path 的值并没有改变,自动实现了"回溯"
            dfs(root->left, path + "->" + to_string(root->left->val));
        }   
        // 同理处理右孩子
        if (root->right) {
            dfs(root->right, path + "->" + to_string(root->right->val));
        }
    }
    vector<string> binaryTreePaths(TreeNode* root) {
        if (root == NULL) return ans; // 鲁棒性判断:空树直接返回
        // 初始化:根节点的值作为路径的起点
        // 注意:这里传入的是 root->val,而不是 "->" + val
        string path = to_string(root->val);
        // 开始递归
        dfs(root, path);
        return ans;
    }
};

总结

1. 核心技巧:值传递代替显式回溯

通常 DFS 题目为了避免空间浪费,我们会传引用 string& path,然后在递归返回时手动 path.pop_back()path.erase()(显式回溯)。

上述代码采用了另一种巧妙的方法:

  • 传值:void dfs(..., string path)
  • 原理:每次调用 dfs 时,path + "->" + val 会生成一个新的字符串副本传递给下一层。
  • 效果:当下一层递归结束返回到本层时,本层的 path 变量从未被修改过,因此不需要写回溯代码,直接处理下一个分支(如右孩子)即可。
  • 代价:虽然代码简洁,但每次递归都会复制字符串,空间开销略大(路径越长,复制开销越大),但在本题数据量下通常可以接受。
2. 代码细节
  • 初始化:在 binaryTreePaths 函数中,先写入根节点的值。这样做是为了保证 dfs 内部的逻辑统一(不用特殊处理开头的 ->)。
  • 拼接时机:在进入下一层递归之前进行拼接。这样做的好处是 dfs 函数接收到的 path 参数就是完整的、可以直接存入 ans 的路径。
3. 复杂度分析
  • 时间复杂度:O(N²)
    • 其中 N 是节点数。需要遍历每个节点一次。
    • 但每个节点都要进行字符串拼接,字符串复制的时间与路径长度成正比。最坏情况(链状树)总复杂度为 1+2+...+N = O(N²)。
  • 空间复杂度:O(N)
    • 主要取决于递归调用栈的深度。虽然字符串复制占用额外空间,但通常视为临时空间,不计入栈空间分析。

404. 左叶子之和

给定二叉树的根节点 root ,返回所有左叶子之和。

cpp 复制代码
class Solution {
public:
    int sum; // 全局变量,用于累加左叶子之和
    // 后序遍历函数
    void getLeftSum(TreeNode* root) {
        if (root == NULL) return;
        // 1. 递归左右子树
        getLeftSum(root->left);
        getLeftSum(root->right);
        // 2. 处理当前节点逻辑
        // 判断当前节点的左孩子是否为"左叶子"
        // 条件:左孩子存在 && 左孩子没有左孩子 && 左孩子没有右孩子(即左孩子是叶子节点)
        if (root->left && !root->left->left && !root->left->right) {
            sum += root->left->val;
        }
    }
    int sumOfLeftLeaves(TreeNode* root) {
        sum = 0; // 初始化
        getLeftSum(root);
        return sum;
    }
};

总结

1. 核心难点:如何判断"左叶子"?

左叶子 的定义是:

  1. 它是某个节点的左孩子。
  2. 它本身是一个叶子节点(左右孩子都为空)。

为什么无法通过节点自身判断?

如果你站在某个节点 node 上,你无法判断自己是不是"左叶子",因为你不知道自己是不是父节点的左孩子。

解决方案:

必须站在父节点的角度看孩子。

  • 代码逻辑:root->left 存在,且 root->left 的左右孩子都为空。
  • 这就是父节点 root 识别左孩子是否为叶子的过程。
2. 复杂度分析
  • 时间复杂度:O(N)
    • 每个节点都会被访问一次。
  • 空间复杂度:O(N)
    • 取决于递归栈的深度,最坏情况(链表)为 O(N)。

222. 完全二叉树的节点个数

给你一棵 完全二叉树 的根节点 root ,求出该树的节点个数。

完全二叉树 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层(从第 0 层开始),则该层包含 1 ~ 2h 个节点。

cpp 复制代码
// 递归法(普通后序遍历)
class Solution {
public:
    int countNodes(TreeNode* root) {
        if (root == NULL) return 0;
        // 后序遍历:先算左子树数量,再算右子树数量
        int left = countNodes(root->left);
        int right = countNodes(root->right);
        // 节点总数 = 左子树 + 右子树 + 1 (当前节点)
        return left + right + 1;
    }
};

// 递归法(利用完全二叉树特性)
class Solution {
public:
    int countNodes(TreeNode* root) {
        if (root == NULL) return 0;
        TreeNode* left = root->left;
        TreeNode* right = root->right;
        int leftdepth = 0, rightdepth = 0;
        // 1. 计算左子树的最左深度(一直往左走)
        while (left) {
            leftdepth++;
            left = left->left;
        }
        // 2. 计算右子树的最右深度(一直往右走)
        while (right) {
            rightdepth++;
            right = right->right;
        }
        // 3. 核心判断
        // 如果左最深度 == 右最深度,说明这棵树是满二叉树
        // 满二叉树节点数公式:2^depth - 1
        // 注意:2 << leftdepth 等价于 2^(leftdepth+1)
        // 代码中 leftdepth 是层数(从0开始算深度),公式应理解为 2^(h) - 1,即 (2 << (h-1)) - 1
        // leftdepth 是遍历计数,实际上代表"除了根节点还有几层"
        // 举例:根节点深度0,left=1,遍历一次 leftdepth=1。
        // 2 << 1 = 4, 4-1 = 3 (根+左+右),正确。
        if (leftdepth == rightdepth) {
            return (2 << leftdepth) - 1; 
        }
        // 4. 如果不是满二叉树,则正常递归
        // 这种递归会迅速触底进入"满二叉树"分支,效率极高
        return countNodes(root->left) + countNodes(root->right) + 1;
    }
};

总结

1. 核心区别
方法 时间复杂度 原理 适用性
普通递归 O(N) 遍历每个节点 任意二叉树
完全二叉树递归 O(logN * logN) 每次判断是否为满二叉树,快速剪枝 仅完全二叉树
2. 为什么第二种方法快?
  • 满二叉树判定:对于完全二叉树,递归下去的过程中,必定有一半的子树是满二叉树。
  • 公式计算:一旦判定为满二叉树,直接用公式返回,省去了该子树所有节点的遍历。
  • 递归深度 :递归深度为树高 O(logN),每层计算深度也是 O(logN),总复杂度 O(log²N)。在数据量大时(如 10^5 节点),O(N) 是 10^5 次运算,O(log²N) 仅约 400 次运算,性能提升巨大。
3. 易错点:位运算
  • 2 << leftdepth 等价于 2*(2^leftdepth)。
  • 因为 leftdepth 是计数出来的,实际上是除了 root 之外的层数。
  • 例如深度为 1 的满二叉树(只有根),循环不执行,leftdepth=0,结果 (2 << 0) - 1 = 1。正确。
  • 例如深度为 2 的满二叉树(根+左+右),循环执行 1 次,leftdepth=1,结果 (2 << 1) - 1 = 3。正确。
相关推荐
进击的小头2 小时前
第11篇:频率响应绘制方法——伯德图(Bode Plot)
python·算法
2401_883035462 小时前
C++20概念(Concepts)入门指南
开发语言·c++·算法
fengci.2 小时前
PolarD&N困难补充
算法
91刘仁德2 小时前
C++ 内存管理
android·c语言·数据结构·c++·经验分享·笔记·算法
sali-tec2 小时前
C# 基于OpenCv的视觉工作流-章38-BF特征匹配
图像处理·人工智能·opencv·算法·计算机视觉
历程里程碑2 小时前
链表-----
数据结构·线性代数·算法·链表·矩阵·lua·perl
一叶落4382 小时前
167. 两数之和 II - 输入有序数组【C语言题解】
c语言·数据结构·算法·leetcode
地平线开发者2 小时前
征程6 MCU safetylib sample
算法·自动驾驶
Barkamin3 小时前
归并排序的简单实现
数据结构