【算法笔记】有人看海,有人被爱,有人做不出 leetcode 第一题(二叉树专题 Part 4)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


Day 14:二叉树


找树左下角的值


题目链接:513. 找树左下角的值 - 力扣(LeetCode)

文章讲解:代码随想录

视频讲解:怎么找二叉树的左下角? 递归中又带回溯了,怎么办?

题目建议:本题递归偏难,反而迭代简单属于模板题, 两种方法掌握一下 ;


image-20250512100851031image-20250512100903989image-20250512100917981image-20250512100930464

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public int findBottomLeftValue(TreeNode root) {
        
    }
}

题目解析


方法一:递归法(DFS)

  • 使用前序遍历(左优先)确保最先访问最左节点
  • 维护最大深度变量,只在叶子节点更新结果
  • 时间复杂度 O(n),空间复杂度 O(h)(递归栈)

class Solution {
    private TreeNode ansNode;
    private int maxDepth = 0;
    public int findBottomLeftValue(TreeNode root) {
        ansNode = root; // err: 初始化, 避免空指针
        dfs(root, 0);
        return ansNode.val;
    }

    private void dfs(TreeNode root, int depth){
        if(root.left == null && root.right == null){
            // 叶子节点, 进入下一步判断
            if(depth > maxDepth){
                // 当前深度大于最大深度, 更新最大深度和最终节点
                maxDepth = depth;
                ansNode = root;
            }
            // 减少后续同一层、更高层不必要的递归
            return;
        }

        if(root.left != null) dfs(root.left, depth + 1);
        if(root.right != null) dfs(root.right, depth + 1);
    }
}

方法二:迭代法(BFS)

  • 层序遍历天然适合找最后一行
  • 每层记录第一个节点,最后保存的就是结果
  • 时间复杂度 O(n),空间复杂度 O(w)(队列)

class Solution {
    public int findBottomLeftValue(TreeNode root) {
        Queue<TreeNode> queue = new LinkedList<>();
        if (root != null) {
            queue.add(root);
        }
        TreeNode ansNode = null;
        while (!queue.isEmpty()) {
            int count = queue.size();
            ansNode = queue.peek();
            while (count > 0) {
                TreeNode cur = queue.poll(); // err: 应该在循环内 poll() 
                if (cur.left != null) queue.add(cur.left);
                if (cur.right != null) queue.add(cur.right);
                count--;
            }
        }
        return ansNode.val;
    }
}

总结

  • 递归更简洁但较难理解回溯

  • 迭代更直观,推荐掌握模板化写法

  • 复杂度对比

方法时间复杂度空间复杂度
递归法O(n)O(h)
迭代法O(n)O(w)

注:h为树高,w为树最大宽度


路径总和


题目链接:112. 路径总和 - 力扣(LeetCode)

文章讲解:代码随想录

视频讲解:拿不准的遍历顺序,搞不清的回溯过程,我太难了!

题目建议:本题又一次涉及到回溯的过程,而且回溯的过程隐藏的还挺深,建议先看视频来理解


image-20250512101135524image-20250512101203819image-20250512101227243image-20250512101240192image-20250512101251395

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public boolean hasPathSum(TreeNode root, int targetSum) {
        
    }
}

题目解析


方法一:递归法(DFS)

  • 使用深度优先搜索(DFS)遍历所有路径
  • 到达叶子节点时判断路径和是否等于目标值
  • 通过参数传递当前剩余的目标值(targetSum - node.val)
  • 时间复杂度 O(n),空间复杂度O(h)(递归栈)

112.路径总和


手动恢复现场:

class Solution {
    public boolean hasPathSum(TreeNode root, int targetSum) {
        if (root == null) return false;
        // 到达叶子节点时判断是否满足条件
        if (root.left == null && root.right == null) {
            return targetSum == root.val;
        }
        // 递归检查左右子树
        return hasPathSum(root.left, targetSum - root.val) || 
               hasPathSum(root.right, targetSum - root.val);
    }
}

自动恢复现场:

class Solution {

    private int target;

    public boolean hasPathSum(TreeNode root, int targetSum) {
        target = targetSum;
        if (root == null) {
            return false;
        }

        return dfs(root, 0); // 递归当前路径的节点和
    }

    private boolean dfs(TreeNode root, int sum) {
        if(root == null){
            return false;
        }

        sum += root.val;

        if (root.left == null && root.right == null && sum == target) {
            return true;
        }
        
        return dfs(root.left, sum) || dfs(root.right, sum);  // err: 不小心写成 &&
    }
}

方法二:迭代法(BFS)

  • 使用模拟递归过程
  • 显式记录每个节点的累计路径和
  • 适合理解递归的回溯过程
  • 时间复杂度O(n),空间复杂度O(n)

class Solution {
    public boolean hasPathSum(TreeNode root, int targetSum) {
        if (root == null) return false;
        Deque<Pair<TreeNode, Integer>> stack = new ArrayDeque<>();
        stack.push(new Pair<>(root, root.val));
        
        while (!stack.isEmpty()) {
            Pair<TreeNode, Integer> node = stack.pop();
            TreeNode cur = node.getKey();
            int sum = node.getValue();
            
            if (cur.left == null && cur.right == null && sum == targetSum) {
                return true;
            }
            
            if (cur.right != null) {
                stack.push(new Pair<>(cur.right, sum + cur.right.val));
            }
            if (cur.left != null) {
                stack.push(new Pair<>(cur.left, sum + cur.left.val));
            }
        }
        return false;
    }
}

总结

返回值选择原则:

  • 当只需要判断是否存在(布尔结果)时,使用返回值
  • 当需要记录所有路径时,不需要返回值(通过参数收集结果)

复杂度对比

方法时间复杂度空间复杂度
递归法O(n)O(h)
迭代法O(n)O(n)

注:n为节点数,h为树高


路径总和Ⅱ


题目链接:113. 路径总和 II - 力扣(LeetCode)


image-20250512101408300image-20250512101423583image-20250512101434875image-20250512101445198image-20250512101454951

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
        
    }
}

题目解析


方法:递归回溯法

  1. 核心逻辑:

    • 使用回溯法遍历所有路径
    • 到达叶子节点时检查路径和是否等于目标值
    • 满足条件时保存当前路径(注意新建列表)
  2. 回溯操作:

    • 添加节点值 → 递归 → 移除节点值(恢复状态)
    • 体现在 path.add()path.remove() 的配对使用
  3. 复杂度分析:

    • 时间复杂度:O(n²)(最坏情况每个路径都符合)
    • 空间复杂度:O(n)(递归栈和路径存储)

注意事项

  1. 结果存储时需 new ArrayList<>(path) 避免引用问题
  2. 先判断节点非空再递归,避免空指针异常
  3. 回溯操作要对称(add/remove 成对出现)

113.路径总和ii

class Solution {

    private int target;
    private List<List<Integer>> ret;

    public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
        target = targetSum;  
        ret = new ArrayList<>();
        List<Integer> path = new ArrayList<>();
        dfs(root, 0, path);
        return ret;
    }

    private void dfs(TreeNode root, int sum, List<Integer> path){
        if(root == null){
            return;
        }
        sum += root.val;
        path.add(root.val);
        if(root.left == null && root.right == null && sum == target){
            ret.add(new ArrayList<>(path));  
            
            // 关键 err: 不能直接 ret.add(path)
            // 由于 path 是一个引用,后续对 path 的修改(如path.remove(path.size() - 1))
            // 会影响到已经添加到 ret 中的路径。因此,最终ret中的路径都是空的。
        }
        
        dfs(root.left, sum, path);
        dfs(root.right, sum, path);
        path.remove(path.size()-1);
        // 易错点: 当对一个节点进行完操作后, 移除这个节点, 而不是递归左右节点后, 分别进行一次移除
    }
}

从中序与后序遍历序列构造二叉树


题目链接:106. 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode)

文章讲解:代码随想录

视频讲解:坑很多!来看看你掉过几次坑 | LeetCode:106.从中序与后序遍历序列构造二叉树

题目建议:本题算是比较难的二叉树题目了,大家先看视频来理解。106.从中序与后序遍历序列构造二叉树105. 从前序与中序遍历序列构造二叉树一起做,思路一样的;


image-20250512200705698image-20250512200720185image-20250512200733427image-20250512200745838

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        
    }
}

题目解析


方法:递归分治法

根据中序和后序遍历序列构造二叉树的方法:

  1. 后序数组的最后一个元素是当前子树的根节点。
  2. 在中序数组中找到根节点的位置,将中序数组分为左子树和右子树。
  3. 根据中序数组的划分,确定后序数组中左子树和右子树的范围
  4. 递归构造:对左子树和右子树重复上述步骤,直到所有子树都被构造完成。

如果手动操作,可以直接根据这两个序列快速画出二叉树:

106.从中序与后序遍历序列构造二叉树


  1. 核心思路:

    • 递归出口 if(iStart > iEnd) return null; ???
    • 递归函数 dfs(iStart, iEnd, pEnd)
    • 重复子问题:
      • pE.val 为值创建本次递归的节点
      • 在中序数组 [iStart, iEnd]中,找到与pE.val相等的分割点 index
      • 递归 [iStart, index-1] ,[index+1, iEnd],作为新的 [iStart, iEnd]
      • 递归左右区间,来连接当前节点的左右子节点(重点:找到两种递归对应的 pE
      • 返回当前节点
  2. 边界条件:

    • inStart > inEnd 时返回 null
    • 后序数组的最后一个元素,总是当前子树的根节点
  3. 复杂度分析:

    • 时间复杂度:O(n) - 每个节点处理一次
    • 空间复杂度:O(n) - 递归栈空间

与先序+中序构造的区别

遍历组合根节点位置左右子树划分依据
前序+中序前序第一个中序根节点位置
后序+中序后序最后一个中序根节点位置

注意事项

  1. 使用索引避免数组拷贝,提升性能
  2. 注意区间闭合范围(本文采用双闭区间)
  3. 计算左子树大小时要减去起始位置
class Solution {
    private int[] inorder;
    private int[] postorder;
    private int len;
    private TreeNode root;

    public TreeNode buildTree(int[] inorder1, int[] postorder1) {
        inorder = inorder1;
        postorder = postorder1;
        len = inorder.length;
        return dfs(0, len - 1 , len - 1);
    }

    public TreeNode dfs(int iStart, int iEnd, int pEnd){
        // err: 设置返回值为 TreeNode, 通过递归来来连接节点

        if(iStart > iEnd){
            // err: iEnd - iStart == 0 不能作为递归出口, 否则会越界???  
            return null;
        }
        
        int index = 0; // index 用于找中序数组分割点
        int rootVal = postorder[pEnd];

        // err: 节点要定义在循环外面, 方便后面递归, 连接左右子节点
        TreeNode root = new TreeNode(rootVal);

        for(int i = iStart; i <= iEnd; i++){
            // err: [iStart, iEnd) 会跳过 iEnd
            if(inorder[i] == rootVal){ 
                // err: 刚开始传的是 len-1, 所以不是[pEnd -1]
                index = i;
                break;
            }
        }

        // err: 递归左右子树时, 区间错误
        // inorder [iStart, index-1] [index+1, iEnd]
        // pEnd = {pEnd - (iEnd - index) - 1} 
        // 先在中序遍历数组找右区间长度 iLen, 再利用 pEnd 和 iLen在后序数组中找左区间的最后一个元素
        
        root.left = dfs(iStart, index-1 , pEnd - (iEnd - index) - 1); 
        root.right = dfs(index+1, iEnd, pEnd-1);

        return root;
    }
}

从前序与中序遍历序列构造二叉树


题目链接:105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)


image-20250512101856427image-20250512101913483image-20250512101927568

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        
    }
}

题目解析


class Solution {
    int[] preorder;
    int[] inorder;
    int len;

    public TreeNode buildTree(int[] preorder1, int[] inorder1) {
        preorder = preorder1;
        inorder = inorder1;
        len = preorder.length;
        return dfs(0, len - 1, 0);
    }

    private TreeNode dfs(int iStart, int iEnd, int pIndex) {
        if (iStart > iEnd) {
            return null;
        }

        int val = preorder[pIndex];
        TreeNode root = new TreeNode(val);
        int index = 0;
        for (int i = iStart; i <= iEnd; i++) {
            if (inorder[i] == val) {
                index = i;
                break;
            }
        }

        root.left = dfs(iStart, index - 1, pIndex + 1);
        root.right = dfs(index + 1, iEnd, pIndex + index - iStart + 1);
        return root;
    }
}

import java.util.*;

class Solution {
    private TreeNode traversal(int[] inorder, int inorderBegin, int inorderEnd, 
                               int[] preorder, int preorderBegin, int preorderEnd) {
        if (preorderBegin == preorderEnd) return null;

        int rootValue = preorder[preorderBegin];
        TreeNode root = new TreeNode(rootValue);

        if (preorderEnd - preorderBegin == 1) return root;

        int delimiterIndex;
        for (delimiterIndex = inorderBegin; delimiterIndex < inorderEnd; delimiterIndex++) {
            if (inorder[delimiterIndex] == rootValue) break;
        }

        // 切割中序数组
        int leftInorderBegin = inorderBegin;
        int leftInorderEnd = delimiterIndex;
        int rightInorderBegin = delimiterIndex + 1;
        int rightInorderEnd = inorderEnd;

        // 切割前序数组
        int leftPreorderBegin = preorderBegin + 1;
        int leftPreorderEnd = preorderBegin + 1 + (delimiterIndex - inorderBegin);
        int rightPreorderBegin = preorderBegin + 1 + (delimiterIndex - inorderBegin);
        int rightPreorderEnd = preorderEnd;

        root.left = traversal(inorder, leftInorderBegin, leftInorderEnd, 
                              preorder, leftPreorderBegin, leftPreorderEnd);
        root.right = traversal(inorder, rightInorderBegin, rightInorderEnd, 
                               preorder, rightPreorderBegin, rightPreorderEnd);

        return root;
    }

    public TreeNode buildTree(int[] preorder, int[] inorder) {
        if (inorder.length == 0 || preorder.length == 0) return null;
        return traversal(inorder, 0, inorder.length, preorder, 0, preorder.length);
    }
}

思考题:前序和后序是否可以唯一确定一棵二叉树?


  • 问题:前序和后序是否可以唯一确定一棵二叉树?

  • 答案:不可以。
    • 前序和中序可以唯一确定一棵二叉树,因为中序遍历可以明确左右子树的分界
    • 后序和中序也可以唯一确定一棵二叉树,原因同上。
    • 前序和后序无法唯一确定一棵二叉树,因为没有中序遍历,无法明确左右子树的分界

  • 例子:
    • tree1 的前序遍历是 [1, 2, 3],后序遍历是 [3, 2, 1]
    • tree2 的前序遍历是 [1, 2, 3],后序遍历是 [3, 2, 1]
    • 这两棵树的前序和后序完全相同,但它们是不同的树。

  1. 问题描述
    • 根据前序遍历和中序遍历的数组重建二叉树。
    • 前序遍历第一个元素是根节点中序遍历根节点的位置可以划分左右子树

  1. 递归逻辑
    • 使用前序遍历的第一个元素确定根节点。
    • 中序遍历中找到根节点的位置,从而划分左右子树
    • 递归地对左右子树进行相同的操作。

  1. 代码实现
    • 使用递归函数traversal,传入当前子树的前序和中序遍历的范围。
    • 通过中序遍历的分界点,确定左右子树的范围。
    • 递归构建左子树和右子树。

  1. 思考题总结
    • 前序和中序后序和中序可以唯一确定一棵二叉树,因为中序遍历提供了左右子树的分界信息
    • 前序和后序无法唯一确定一棵二叉树,因为缺少左右子树的分界信息。

  1. 调试建议
    • 在实现复杂递归逻辑时,建议添加日志打印,观察递归过程是否符合预期。
    • 避免仅通过脑动模拟,实际编码并调试是掌握这类问题的关键。

在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值