代码随想录-二叉树

78 阅读1分钟

代码随想录之二叉树篇

T144-二叉树的前序遍历

见力扣第144题[二叉树的前序遍历]

题目描述

给你二叉树的根节点 root ,返回它节点值的 前序 遍历。

我的思路

  • 使用递归遍历的方式
  • 前序遍历是指遍历的顺序为:父节点 -> 左孩子-> 右孩子
List<Integer> resList = new ArrayList<>();

/**
 * 二叉树的前序遍历
 * @param root
 * @return
 */
public List<Integer> preorderTraversal(TreeNode root) {
    traverse(root);
    return resList;
}

private void traverse(TreeNode root) {
    if (root == null) return;
    resList.add(root.val);

    traverse(root.left);
    traverse(root.right);
}
  • 时间复杂度为:O(N)O(N),每个节点都要被访问一次
  • 空间复杂度为:O(N)O(N),递归过程中栈的开销,平均情况下为O(logN)O(\log N)

T102-二叉树的层序遍历

见力扣第102题[二叉树的层序遍历]

题目描述

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

我的思路

  • 使用队列存储每一层的节点
  • 遍历每一层的节点,将孩子节点入队
public List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> levelList = new ArrayList<>();
    LinkedList<TreeNode> q = new LinkedList<>();
    if (root != null) q.offer(root);
    while (!q.isEmpty()) {
        int sz = q.size();
        List<Integer> temp = new ArrayList<>();
        for (int i = 0; i < sz; i++) {
            TreeNode cur = q.pollFirst();
            temp.add(cur.val);
            if (cur.left != null) q.offer(cur.left);
            if (cur.right != null) q.offer(cur.right);
        }
        levelList.add(temp);
    }
    return levelList;
}
  • 时间复杂度:O(N)O(N),每个节点都要访问一次
  • 空间复杂度:O(N)O(N),额外的队列存储每一层的节点

T107-二叉树的层序遍历II

见力扣第107题[二叉树的层序遍历II]

题目描述

给你二叉树的根节点 root ,返回其节点值 自底向上的层序遍历 。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)

我的思路

  • 和T102的解题思路相同,将层序遍历放入一个栈中
  • 循环结束之后,将栈中的临时结果集弹出到结果集中

T199-二叉树的右视图

见力扣第199题[二叉树的右视图]

题目描述

给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。

image-20241030153051068

示例

输入: [1,2,3,null,5,null,4]
输出: [1,3,4]

我的思路

  • 二叉树的层序遍历,将层序中最后一个元素存储到结果集合中
public List<Integer> rightSideView(TreeNode root) {
    List<Integer> resList = new ArrayList<>();
    Queue<TreeNode> q = new ArrayDeque<>();
    if (root != null) q.offer(root);
    while (!q.isEmpty()) {
        int sz = q.size();
        for (int i = 0; i < sz; i++) {
            TreeNode cur = q.poll();
            if (i == sz - 1) {
                resList.add(cur.val);
            }
            if (cur.left != null) q.offer(cur.left);
            if (cur.right != null) q.offer(cur.right);
        }
    }
    return resList;
}
  • 时间复杂度:O(N)O(N),每个节点都要被访问一次
  • 空间复杂度:O(N)O(N),每个节点最多进入队列一次,队列的最大长度不超过节点数量

T637-二叉树的层平均值

见力扣第637题[二叉树的层平均值]

题目描述

给定一个非空二叉树的根节点 root , 以数组的形式返回每一层节点的平均值。与实际答案相差 10-5 以内的答案可以被接受。

我的思路

  • 层序遍历,记录每层的节点值总和
  • 计算出平均值,存储到结果集合中
public List<Double> averageOfLevels(TreeNode root) {
    List<Double> ansList = new ArrayList<>();
    Queue<TreeNode> q = new LinkedList<>();
    if (root != null) q.offer(root);

    while (!q.isEmpty()) {
        int sz = q.size();
        double sum = 0.0;
        for (int i = 0; i < sz; i++) {
            TreeNode cur = q.poll();
            sum += cur.val;
            if (cur.left != null) q.offer(cur.left);
            if (cur.right != null) q.offer(cur.right);
        }
        ansList.add((double)(sum / sz));
    }
    return ansList;
}

T429-N叉树的层序遍历

见力扣第429题[N叉树的层序遍历]

题目描述

给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。

树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。

我的思路

  • N叉树的层序遍历和二叉树的层序遍历大差不差
  • 都是将非空的孩子节点放入到队列中
public List<List<Integer>> levelOrder(Node root) {
    List<List<Integer>> resList = new ArrayList<>();
    Queue<Node> q = new LinkedList<>();
    if (root != null) q.offer(root);

    while (!q.isEmpty()) {
        int sz = q.size();
        List<Integer> temp = new ArrayList<>();
        for (int i = 0; i < sz; i++) {
            Node cur = q.poll();
            temp.add(cur.val);
            for (Node node : cur.children) {
                if (node != null) {
                    q.offer(node);
                }
            }

        }
        resList.add(temp);
    }
    return resList;
}
  • 时间复杂度:O(N)O(N)
  • 空间复杂度:O(N)O(N)

T515-每个树行中寻找最大值

见力扣第515题[每个树行中寻找最大值]

题目描述

给定一棵二叉树的根节点 root ,请找出该二叉树中每一层的最大值。

我的思路

  • 层序遍历
  • 遍历每一层的时候,更新最大值
public List<Integer> largestValues(TreeNode root) {
    List<Integer> resList = new ArrayList<>();
    Queue<TreeNode> q = new LinkedList<>();

    if (root != null) q.offer(root);
    while (!q.isEmpty()) {
        int sz = q.size();
        int max = Integer.MIN_VALUE;
        for (int i = 0; i < sz; i++) {
            TreeNode cur = q.poll();
            max = Math.max(max, cur.val);
            if (cur.left != null) q.offer(cur.left);
            if (cur.right != null) q.offer(cur.right);
        }
        resList.add(max);
    }
    return resList;
}

T116-填充每个节点的下一个右侧节点指针

见力扣第116题[填充每个节点的下一个右侧节点指针]

题目描述

给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。

填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL

初始状态下,所有 next 指针都被设置为 NULL

image-20241020213644842

我的思路

  • 使用二叉树的层序遍历
  • 每一层中,将当前节点的next指针赋值给q.peek()
public Node connect(Node root) {
    Queue<Node> q = new LinkedList<>();
    if (root != null) q.offer(root);

    while (!q.isEmpty()) {
        int sz = q.size();
        for (int i = 0; i < sz; i++) {
            Node cur = q.poll();
            if (i == sz - 1) {
                cur.next = null;
            } else {
                cur.next = q.peek();
            }
            if (cur.left != null) q.offer(cur.left);
            if (cur.right != null) q.offer(cur.right);
        }
    }
    return root;
}

解法二

  • 当前层已经是一个链表了
  • 遍历当前层的链表节点的子节点,构造下一层的链表
  • 移动到一下层的实际头结点处
public Node connectII(Node root) {
    if (root == null) return null;

    Node head = root;

    // 循环遍历每一层链表
    while (head != null) {
        // 构建下一层的链表
        Node dummy = new Node();
        Node temp = dummy;

        // 遍历当前链表
        Node cur = head;
        while (cur != null) {
            if (cur.left != null) {
                temp.next = cur.left; // 构建子节点链表
                temp = temp.next; // 指针移动
            }
            if (cur.right != null) {
                temp.next = cur.right;
                temp = temp.next;
            }
            cur = cur.next;
        }
        // 移动到下一层的头结点处
        head = dummy.next;
    }
    return root;
}
  • 时间复杂度:O(N)O(N),访问每一个节点
  • 空间复杂度:O(1)O(1),常数级的空间存储临时指针

T104-二叉树的最大深度

见力扣第104题[二叉树的最大深度]

题目描述

给定一个二叉树 root ,返回其最大深度。

二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。

我的思路

  • 后序遍历,分别获取左右子节点的深度
  • 然后返回当前子树的深度
public int maxDepth(TreeNode root) {
        if (root == null) return 0;
        
        // 获取左子树的最大深度
        int maxLeft = maxDepth(root.left);
        
        // 获取右子树的最大深度
        int maxRight = maxDepth(root.right);
        
        // 当前子树的深度为
        return Math.max(maxLeft, maxRight) + 1;
    }

T111-树的最小深度

见力扣第111题[树的最小深度]

题目描述

给定一个二叉树,找出其最小深度。

最小深度是从根节点到最近叶子节点的最短路径上的节点数量。

**说明:**叶子节点是指没有子节点的节点。

我的思路

  • 使用BFS搜索,每遍历一层孩子节点,最小深度就加1
  • 如果某个节点没有子节点了,直接返回minDepth
public int minDepth(TreeNode root) {
    if (root == null) return 0;
    Queue<TreeNode> q = new LinkedList<>();
    q.offer(root);
    int minDepth = 1;
    while (!q.isEmpty()) {
        int sz = q.size();
        for (int i = 0; i < sz; i++) {
            TreeNode cur = q.poll();
            if (cur.left == null && cur.right == null) {
                return minDepth;
            }
            if (cur.left != null) {
                q.offer(cur.left);
            }
            if (cur.right != null) {
                q.offer(cur.right);
            }
        }
        minDepth++;
    }
    return minDepth;
}

T226-反转二叉树

见力扣第226题[反转二叉树]

题目描述

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

image-20241031151511677

我的思路

  • 使用递归遍历的方式,对树的后序位置进行操作
  • 如果root节点为空,则返回null
  • 获得反转之后的左孩子和右孩子
  • root的左指针赋给右孩子,右指针赋给左孩子
/**
 * 翻转二叉树
 * @param root
 * @return
 */
public TreeNode invertTree(TreeNode root) {

    if (root == null) return null;

    return invert(root);

}

/**
 * 递归的方式翻转二叉树
 * @param root
 * @return
 */
private TreeNode invert(TreeNode root) {
    if (root == null) return null;

    TreeNode left = invertTree(root.left);
    TreeNode right = invertTree(root.right);

    root.left = right;
    root.right = left;

    return root;
}
  • 时间复杂度:O(N)O(N),需要遍历每个节点
  • 空间复杂度:O(N)O(N),递归栈所用空间最差为N

T101-对称二叉树

见力扣第101题[对称二叉树]

题目描述

给你一个二叉树的根节点 root , 检查它是否轴对称。

我的思路

  • 使用递归遍历的方式
  • 判断这颗树是否堆成,就是判断树的两个子节点是否是对称的
  • 递归函数可以写为symmetric(left, right)
/**
 * 判断是否为对称的二叉树
 * @param root
 * @return
 */
public boolean isSymmetric(TreeNode root) {
    if (root == null) return true;

    // 判断两个子树是否符合轴对称
    return symmetric(root.left, root.right);
}

/**
 * 判断两个树是否轴对称
 * @param left
 * @param right
 */
private boolean symmetric(TreeNode left, TreeNode right) {
    if (left == null && right != null || right == null && left != null) return false;
    if (left == null && right == null) return true;

    // 两个树都不是 null
    if (left.val != right.val) return false;
    return symmetric(left.right, right.left) && symmetric(left.left, right.right);
}

时间复杂度:O(N)O(N),需要遍历树的每个节点

空间复杂度:O(N)O(N),递归栈占用的空间最多为NN

T222-完全二叉树的节点个数

见力扣第222题[完全二叉树的节点个数]

题目描述

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

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

我的思路

  • 前序遍历,每遍历一个节点,计数器加1
  • 返回计数器的值

对于普通的树可以使用这种解法,但是题目中是有完全二叉树的可用信息的

思路

  • 对于一个满二叉树,他的节点个数可以直接用公式计算出来,即
nums=2n1nums = 2^n - 1
  • 其中nn为树的深度
  • 首先判断该节点是否是满二叉树,计算出来树的深度,直接返回结果
  • 如果不是的话,就递归计算树的左右孩子节点,加上当前节点
/**
 * 计算当前完全二叉树的节点个数
 * @param root
 * @return
 */
public int countNodes(TreeNode root) {
    if (root == null) return 0;
    TreeNode left = root.left;
    TreeNode right = root.right;
    // 判断当前二叉树是否为满二叉树
    int lDepth = 1;
    int rDepth = 1;
    while (left != null) {
        lDepth++;
        left = left.left;
    }
    while (right != null) {
        rDepth++;
        right = right.right;
    }
    // 最左和最右子树的高度一样,是满二叉树,公式求解
    if (rDepth == lDepth) {
        return 1 << rDepth - 1;
    }
    // 不是满二叉树
    return countNodes(root.left) + countNodes(root.right) + 1;
}
  • 时间复杂度满足T(n)=T(n/2)+lognT(n)=T(n/2)+\log n,求得为O(log2n)O(\log^2 n)
  • 空间复杂度:O(logN)O(\log N),递归的深度

T110-平衡二叉树

见力扣第110题[平衡二叉树]

题目描述

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

平衡二叉树 是指该树所有节点的左右子树的深度相差不超过 1。

image-20241031161210568

输入:root = [3,9,20,null,null,15,7]
输出:true

我的思路

  • 使用递归遍历的方式,求出左右子树的最大深度
  • 如果两个子树的深度差距超过1,则直接返回false
  • 否则返回true
public boolean isBalancedI(TreeNode root) {
    if (root == null) return true;

    return height(root) >= 0;
}

/**
 * 判断树的深度
 * @param root
 * @return
 */
private int height(TreeNode root) {

    if (root == null) return 0;

    int lHeight = height(root.left);
    int rHeight = height(root.right);

    if (lHeight == -1 || rHeight == -1 || Math.abs(rHeight - lHeight) > 1) {
        return -1;
    }
    else {
        return Math.max(lHeight, rHeight) + 1;
    }
}
  • 时间复杂度:O(N)O(N),采用自底向上的递归(后序位置),每个节点的计算高度和判断平衡都只需要处理一次
  • 空间复杂度:O(N)O(N),最坏的情况下,需要递归NN

T257-二叉树的所有路径

见力扣第257题[二叉树的所有路径]

题目描述

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

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

image-20241031164030855

输入:root = [1,2,3,null,5]
输出:["1->2->5","1->3"]

我的思路

  • 树的深度优先遍历
  • 使用回溯算法实现
List<String> paths = new ArrayList<>();
/**
 * 二叉树的所有路径
 * @param root
 * @return
 */
public List<String> binaryTreePaths(TreeNode root) {
    if (root == null) return paths;
    List<Integer> path = new ArrayList<>(); // 记录路径
    backtrace(root, path);
    return paths;
}

/**
 * 回溯的方式获得二叉树的所有路径
 * @param root
 * @param sb
 */
private void backtrace(TreeNode root, List<Integer> path) {
    // 首先将当前节点添加到路径中
   path.add(root.val);
    // 如果当前节点没有孩子节点,说明到头了,是一条路径
    if (root.left == null && root.right == null) {
        String res = toPath(path);
        // 将路径加入到结果集合中
        paths.add(res);
    }
    if (root.left != null) {
        // 遍历左孩子
        backtrace(root.left, path);
    }
    if (root.right != null) {
        // 遍历右孩子
        backtrace(root.right, path);
    }
    // 将当前节点从路径中取消
    path.remove(path.size() - 1);
}

private String toPath(List<Integer> path) {
    StringBuilder sb = new StringBuilder();
    for (int num : path) {
        sb.append(num).append("->");
    }
    return sb.substring(0, sb.length() - 2); // 去掉最后一个箭头
}
  • 时间复杂度:O(N2)O(N^2),需要遍历每一个子树
  • 空间复杂度:O(logN×logN)O(\log N \times \log N),当二叉树为平衡二叉树的时候,高度为logN\log N

T404-左叶子之和

见力扣第404题[左叶子之和]

题目描述

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

我的思路

  • 树的深度优先遍历,左叶子节点的位置在
  • root.left != null && root.left has none child
private int counts = 0;
/**
 * 所有左叶子节点的和
 * @param root
 * @return
 */
public int sumOfLeftLeaves(TreeNode root) {
    if (root == null) return counts;
    findLeftLeaves(root);
    return counts;
}

/**
 * 递归找到左叶子
 * @param root
 */
private void findLeftLeaves(TreeNode root) {
    if (root == null) return;
    // 当前节点是非叶子节点
    TreeNode leftNode = root.left;
    TreeNode rightNode = root.right;
    // 找到左叶子:左叶子没孩子
    if (leftNode != null && leftNode.left == null && leftNode.right == null) {
        counts += leftNode.val;
    }
    // 递归遍历左右孩子
    findLeftLeaves(leftNode);
    findLeftLeaves(rightNode);
}
  • 时间复杂度:O(N)O(N)
  • 空间复杂度:O(N)O(N)

T513-找到树最下角的值

见力扣第513题[找到树左下角的值]

题目描述

给定一个二叉树的 根节点 root,请找出该二叉树的 最底层 最左边 节点的值。

假设二叉树中至少有一个节点。

我的思路

  • 树的层序遍历
  • 记录最后一层,最左边的数字

思路二

  • 树的深度优先搜索
  • 记录叶子节点的height, val
  • 如果curHeight > height,更新
  • 先遍历左叶子,再遍历右叶子
private int height;
private int val;
/**
 * 递归遍历
 * @param root
 * @return
 */
public int findBottomLeftValueI(TreeNode root) {
    if (root == null) return 0;
    traverse(root, 1);
    return val;
}

/**
 * 递归遍历整棵树
 * @param root
 */
private void traverse(TreeNode root, int curHeight) {
    if (curHeight > height) {
        val = root.val;
        height = curHeight;
    }
    // 遍历左节点
    if (root.left != null) { traverse(root.left, curHeight + 1); }
    if (root.right != null) { traverse(root.left, curHeight + 1); }
}

T112-路径总和

见力扣第112题[路径总和]

题目描述

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false

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

我的思路

  • 使用回溯,递归遍历每个节点,更新路径和
  • 如果到达叶子节点的时候路径和为sum,则将标记available置为true
private boolean available = false;
/**
 * 是否存在路径和为 sum 的路径
 * @param root
 * @param targetSum
 * @return
 */
public boolean hasPathSum(TreeNode root, int targetSum) {
    if (root == null) return false;
    int curSum = 0;
    backtrace(root, curSum, targetSum);
    return available;
}

/**
 * 回溯 判断路径和是否为 targetSum
 * @param root
 * @param curSum
 */
private void backtrace(TreeNode root, int curSum, int targetSum) {
    if (available || root == null) return;
    // 将当前值添加到路径中
    curSum += root.val;
    // 当前是叶子节点
    if (root.left == null && root.right == null ) {
        if (curSum == targetSum) {
            this.available = true; // 标记为置为 true
        }
        return;
    }
    // 遍历左右叶子节点
    if (root.left != null) {
        backtrace(root.left, curSum, targetSum);
    }
    if (root.right != null) {
        backtrace(root.right, curSum, targetSum);
    }
    // 将当前节点移出路径
    curSum -= root.val;
}
  • 时间复杂度:O(N)O(N),最坏的情况下,需要对每个节点都要访问一次
  • 空间复杂度:O(H)O(H),其中HH是树的高度,取决于递归时间栈空间的开销。

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

见力扣第106题[从中序与后序遍历序列构造二叉树]

题目描述

给定两个整数数组 inorderpostorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树

示例

输入:inorder = [9,3,15,20,7], postorder = [9,15,7,20,3]
输出:[3,9,20,null,null,15,7]

我的思路

  • 中序遍历数组的构成为:左子树 | 根节点 | 右子树
  • 后序遍历数组的构成为:左子树 | 右子树 | 根节点
  • 在后续遍历数组中,根节点固定是在最后一位的
  • 可以根据根节点的值,找到中序遍历根节点的索引位置,分出左子树和右子树的范围
Map<Integer, Integer> inMap = new HashMap<>();
/**
 * 从中序和后序遍历数组中构建树
 * @param inorder
 * @param postorder
 * @return
 */
public TreeNode buildTree(int[] inorder, int[] postorder) {
    // 初始化存储中序遍历索引和值的map
    for (int i = 0; i < inorder.length; i++) {
        inMap.put(inorder[i], i);
    }
    int inStart = 0;
    int inEnd = inorder.length - 1;
    int postStart = 0;
    int postEnd = postorder.length - 1;

    return build(inorder, inStart, inEnd, postorder, postStart, postEnd);
}

/**
 * 递归构建树
 * @param inorder
 * @param inStart
 * @param inEnd
 * @param postorder
 * @param postStart
 * @param postEnd
 * @return
 */
private TreeNode build(int[] inorder, int inStart, int inEnd, int[] postorder, int postStart, int postEnd) {
    if (postStart > postEnd) return null;
    // 取出根节点
    int rootVal = postorder[postEnd];
    TreeNode root = new TreeNode(rootVal);
    int rootIndex = inMap.get(rootVal);

    // 获取左子树的数量
    int leftCounts = rootIndex - inStart;
    // 获取左子树在后续遍历的范围
    int lPostStart = postStart;
    int lPostEnd = postStart + leftCounts - 1;
    // 左子树在中序遍历的范围
    int lInStart = inStart;
    int lInEnd = rootIndex - 1;

    // 递归创建左子树
    TreeNode leftNode = build(inorder, lInStart, lInEnd, postorder, lPostStart, lPostEnd);

    // 获取右子树在中序和后序的范围
    int rInStart = rootIndex + 1;
    int rInEnd = inEnd;
    int rPostStart = lPostEnd + 1;
    int rPostEnd = postEnd - 1;

    // 递归创建右子树
    TreeNode rightNode = build(inorder, rInStart, rInEnd, postorder, rPostStart, rPostEnd);

    root.left = leftNode;
    root.right = rightNode;
    return root;
}
  • 时间复杂度:O(N)O(N),其中NN为树的节点个数
  • 空间复杂度:O(N)O(N),需要使用O(N)O(N)的空间存储哈希表。

T654-创建最大二叉树

见力扣第654题[创建最大二叉树]

题目描述

给定一个不重复的整数数组 nums最大二叉树 可以用下面的算法从 nums 递归地构建:

  1. 创建一个根节点,其值为 nums 中的最大值。
  2. 递归地在最大值 左边子数组前缀上 构建左子树。
  3. 递归地在最大值 右边子数组后缀上 构建右子树。

返回 nums 构建的 *最大二叉树*

示例 1:

输入:nums = [3,2,1,6,0,5]
输出:[6,3,5,null,2,0,null,null,1]
解释:递归调用如下所示:
- [3,2,1,6,0,5] 中的最大值是 6 ,左边部分是 [3,2,1] ,右边部分是 [0,5] 。
    - [3,2,1] 中的最大值是 3 ,左边部分是 [] ,右边部分是 [2,1] 。
        - 空数组,无子节点。
        - [2,1] 中的最大值是 2 ,左边部分是 [] ,右边部分是 [1] 。
            - 空数组,无子节点。
            - 只有一个元素,所以子节点是一个值为 1 的节点。
    - [0,5] 中的最大值是 5 ,左边部分是 [0] ,右边部分是 [] 。
        - 只有一个元素,所以子节点是一个值为 0 的节点。
        - 空数组,无子节点。

我的思路

  • 在数组的指定范围内,找到最大值及其坐标,构建根节点
  • 根据左右子树的范围,分别递归创建左右子树
  • 构建树结构,返回根节点
/**
 * 创建最大二叉树
 * @param nums
 * @return
 */
public TreeNode constructMaximumBinaryTree(int[] nums) {
    if (nums == null || nums.length <= 0) return null;
    
    int start = 0;
    int end = nums.length - 1;
    return construct(nums, start, end);
    
}

/**
 * 递归创建最大二叉树
 * @param nums
 * @param start
 * @param end
 * @return
 */
private TreeNode construct(int[] nums, int start, int end) {
    if (start > end) return null;
    int maxVal = Integer.MIN_VALUE;
    int maxIndex = -1;
    // 找到最大值及其坐标
    for (int i = start; i <= end; i++) {
        if (nums[i] > maxVal) {
            maxVal = nums[i];
            maxIndex = i;
        }
    }
    // 创建根节点
    TreeNode root = new TreeNode(maxVal);
    // 递归创建左右孩子节点
    TreeNode leftNode = construct(nums, start, maxIndex - 1);
    TreeNode rightNode = construct(nums, maxIndex + 1, end);
    
    // 构建树
    root.left = leftNode;
    root.right = rightNode;
    
    return root;
}
  • 时间复杂度:O(N2)O(N^2),最坏的情况下,需要递归NN层,并且遍历O(N)O(N)个元素寻找最大值
  • 空间复杂度:O(N)O(N),最坏的情况下,递归栈所需要的栈空间

思路二[单调栈]

TODO

T617-合并二叉树

见力扣第617题[合并二叉树]

题目描述

给你两棵二叉树: root1root2

想象一下,当你将其中一棵覆盖到另一棵之上时,两棵树上的一些节点将会重叠(而另一些不会)。你需要将这两棵树合并成一棵新二叉树。合并的规则是:如果两个节点重叠,那么将这两个节点的值相加作为合并后节点的新值;否则,不为 null 的节点将直接作为新二叉树的节点。

返回合并后的二叉树。

注意: 合并过程必须从两个树的根节点开始。

我的思路

  • 将两颗二叉树的平面图覆盖在一起,相交相加
  • 递归构建树
/**
 * 合并二叉树
 * @param root1
 * @param root2
 * @return
 */
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
    if (root1 == null) return root2;
    else if (root2 == null) return root1;

    // 将两个节点的值相加
    TreeNode root = new TreeNode(root1.val + root2.val);
    TreeNode leftNode = mergeTrees(root1.left, root2.left);
    TreeNode rightNode = mergeTrees(root1.right, root2.right);

    // 构建树
    root.left = leftNode;
    root.right = rightNode;

    return root;
}
  • 时间复杂度:O(min{M,N})O(\min \{M, N\}),其中MMNN分别为两个二叉树的节点。只有两个树的节点都不空的时候,才会执行合并,因此被访问到的节点不会超过较小的二叉树的节点
  • 空间复杂度:O(min{M,N})O(\min \{M, N\}),取决于递归调用的层数,不会超过较小二叉树的最大高度

T700-二叉搜索树中的搜索

见力扣第700题[二叉搜索树中的搜索]

题目描述

给定二叉搜索树(BST)的根节点 root 和一个整数值 val

你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 null

我的思路

  • 根据二叉搜索树的特征,递归搜索
/**
 * BST 中的搜索
 * @param root
 * @param val
 * @return
 */
public TreeNode searchBST(TreeNode root, int val) {
    if (root == null || root.val == val) return root;
    if (root.val > val) {
        // 搜索左子树
        searchBST(root.left, val);
    } else {
        searchBST(root.right, val);
    }
}
  • 时间复杂度:O(logN)O(\log N),二分法
  • 空间复杂度:O(logN)O(\log N),最好的情况下,树为满二叉树,树高为logN\log N

T98-验证搜索二叉树

见力扣第98题[验证搜索二叉树]

题目描述

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

  • 节点的左子树只包含 小于 当前节点的数。
  • 节点的右子树只包含 大于 当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。

我的思路

  • 左子树的每个节点都小于根节点,有最大值的限制
  • 右子树的每个节点都大于根节点,有最小值的限制
public boolean isValidBST(TreeNode root) {

        TreeNode max = null;
        TreeNode min = null;
        return valid(root, max, min);
    }

    /**
     * 递归验证是否是 BST
     * @param root
     * @param max
     * @param min
     * @return
     */
    private boolean valid(TreeNode root, TreeNode max, TreeNode min) {
        if (root == null) return true;
        // 如果有限定最大值,说明是左子树,左子树大于最大值,返回false
        if (max != null && root.val >= max.val) return false;
        
        // 有限定最小值,说明是右子树,右子树小于最小值,返回false
        if (min != null && root.val <= min.val) return false;
        
        // 左子树最大值为 root,右子树最小值为 root
        return valid(root.left, root, min)
                && valid(root.right, max, root);
    }
  • 时间复杂度:O(N)O(N),每个二叉树节点最多会被访问一次
  • 空间复杂度:O(N)O(N),递归栈最坏的情况下需要递归NN

T530-二叉搜索树的最小绝对差

见力扣第530题[二叉搜索树的最小绝对差]

题目描述

给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值

差值是一个正数,其数值等于两值之差的绝对值。

示例

image-20241101170938985

输入:root = [4,2,6,1,3]
输出:1

我的思路

  • BST的中序遍历是一个有序数组
  • 遍历数组,两两相减更新最小差值
List<Integer> inorder = new ArrayList<>();
/**
 * BST 中最小绝对差值
 * @param root
 * @return
 */
public int getMinimumDifference(TreeNode root) {
   // 前序遍历 BST
    traverse(root);
    int min = Integer.MAX_VALUE;
    for (int i = 1; i < inorder.size(); i++) {
        min = Math.min(min, inorder.get(i) - inorder.get(i - 1));
    }
    return min;
}

/**
 * BST 的中序遍历
 * @param root
 */
private void traverse(TreeNode root) {
    if (root == null) return;

    traverse(root.left);

    inorder.add(root.val);

    traverse(root.right);
}

优化方式

  • 在中序遍历的时候,更新最小的值
private int ans = Integer.MAX_VALUE;
private int pre = -1; // 遍历的前一个节点
/**
 * 在前序遍历的时候,更新答案
 * @param root
 * @return
 */
public int getMinimumDifferenceI(TreeNode root) {
    inorder(root);
    return ans;
}

private void inorder(TreeNode root) {
    if (root == null) return;

    inorder(root.left);

    if (pre != -1) {
        // 遍历过节点
        ans = Math.min(ans, root.val - pre);
    }
    pre = root.val;

    inorder(root.right);
}
  • 时间复杂度:O(N)O(N),遍历二叉树中的每个节点
  • 空间复杂度:O(N)O(N),最坏情况下二叉树为一个链表,需要的栈空间为O(N)O(N)

T501-二叉搜索树中的众数

见LeetCode第501题[二叉搜索树中的众数]

题目描述

给你一个含重复值的二叉搜索树(BST)的根节点 root ,找出并返回 BST 中的所有 众数(即,出现频率最高的元素)。

如果树中有不止一个众数,可以按 任意顺序 返回。

假定 BST 满足如下定义:

  • 结点左子树中所含节点的值 小于等于 当前节点的值
  • 结点右子树中所含节点的值 大于等于 当前节点的值
  • 左子树和右子树都是二叉搜索树

示例

输入:root = [1,null,2,2]
输出:[2]

我的思路

  • 众数,就是出现频率,我们可以通过前序遍历,记录每个数字出现的频率
  • 然后将出现数字和出现频率以key, val的方式存储到Map
  • 最后取出若干个众数

优化思路

  • 由于BST的中序遍历是一个有序数组,重复的元素都是在一起的
  • 所以我们可以使用额外的变量maxCount | count | cur来记录中间状态
    • maxCount:表示出现的最大频率
    • cur:当前数字
    • count:当前数字出现的频率
List<Integer> ans = new ArrayList<>();

// 初始化状态数据
int maxCount = 1; // 最大频次
int cur = 0; // 当前数据
int count = 0; // 当前频次

/**
 * 寻找 BST 中的众数
 * @param root
 * @return
 */
public int[] findMode(TreeNode root) {

    if (root == null) return null;


    // 对 root 进行中序遍历
    traverse(root);

    // 将 ans List转换为静态数组
    int[] res = new int[ans.size()];
    for (int i = 0; i < ans.size(); i++) {
        res[i] = ans.get(i);
    }

    return res;


}

/**
 * 中序遍历
 * @param root
 * @param maxCount
 * @param cur
 * @param count
 * @return
 */
private void traverse(TreeNode root) {

    if (root == null) return;

    traverse(root.left);

    // 中序遍历的逻辑
    if (root.val == cur) {
        // 1 更新当前数字的频次
        count++;
    } else {
        count = 1; // 复位
        cur = root.val;
    }

    // 根据count 和 maxCount的大小关系来维护结果数组
    if (count == maxCount) {
        ans.add(cur);
    } else if (count > maxCount) {
        // 更新最大频次
        ans.clear();
        ans.add(cur);
        maxCount = count;
    }


    traverse(root.right);
}

T236-二叉树最近的公共祖先

见LeetCode第236题[二叉树最近的公共祖先]

题目描述

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。

示例

image-20241120200504713

输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3

我的思路

  • DFS深度搜索,记录两个目标节点的搜索路径,也就是他们的父节点
  • List<TreeNode> parents1List<TreeNode> parents2,哪个元素多,说明他的位置更深
  • 将元素少的数组元素都存储到set中,对另一个数组从队尾开始遍历,判断是否存在于set中,找到的即为两个公共祖先

思路二

  • 使用分解问题的思路
  • 站在当前节点,我们应该考虑什么?
    • if (root == p || root == q || root == null),应该直接返回return root
    • 递归从左子树中查找,获取结果leftNode
    • 递归从右子树中查找,获取结果rightNode
    • 如果leftNode节点是空的,代表左子树中找不到节点pq,返回rightNode
    • 如果两个节点都是非空的,表示两个节点分别位于两个子树,则公共祖先为root

实现代码

public TreeNode lowestCommonAncestorI(TreeNode root, TreeNode p, TreeNode q) {
    if (root == null || root == p || root == q) return root;

    // 从左边子树找到的最近的公共祖先
    TreeNode leftParent = lowestCommonAncestor(root.left, p, q);
    // 从右子树中找到最近的公共祖先
    TreeNode rightParent = lowestCommonAncestor(root.right, p, q);

    if (leftParent == null) return rightParent;
    if (rightParent == null) return leftParent;
    return root;
}

T669-修剪搜索二叉树

见Leetcode第669题修剪搜索二叉树

题目描述 给你二叉搜索树的根节点 root ,同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在[low, high]中。修剪树 不应该 改变保留在树中的元素的相对结构 (即,如果没有被移除,原有的父代子代关系都应当保留)。 可以证明,存在 唯一的答案 。

所以结果应当返回修剪好的二叉搜索树的新的根节点。注意,根节点可能会根据给定的边界发生改变

示例

Snipaste_2024-12-14_10-43-20.png 我的思路

  • 一定要用分解问题的思路去思考
  • 站在当前节点:
    • 当前节点的返回值什么含义:返回的是以当前节点为根节点,符合要求的新 BST
  • 如何去分解问题?
    • 当前节点符合范围:
      • 分别获取修剪之后的左右孩子节点
      • 构建新的树,并返回
    • 当前节点小于范围:
      • 直接返回修剪之后的右孩子
    • 当前节点大于范围:
      • 直接返回修剪之后的左孩子

题解

public TreeNode trimBST(TreeNode root, int low, int high) {
    if(root == null) return null;
    if (root.val < low) {
        return trimBST(root.right, low, high);
    }
    else if (root.val > high) {
        return trimBST(root.left, low, high);
    } else {
        TreeNode left = trimBST(root.left, low, high);
        TreeNode right = trimBST(root.right, low, high);
        root.left = left;
        root.right = right;
        return root;
    }
}

复杂度分析

  • 时间复杂度:O(N)O(N),最坏情况下每个节点都需要遍历一次
  • 空间复杂度:O(N)O(N),使用的递归栈所占用的空间

T108-将有序数组转换为二叉搜索树

见Leecode第108题将有序数组转换为二叉搜索树

题目描述

给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵  平衡二叉搜索树。

我的思路

  • 以分解问题的思维解决
  • 子方法的返回值是什么?是已经转换好的BST树
  • 站在当前节点,我们需要做如下的事情:
    • 如果当前跟节点索引超过了索引范围,直接返回null
    • 以中间索引的值新建根节点
    • 递归获取左右孩子节点,注意索引的变换
    • 构建树,并返回

实现代码

/**
     * 将有序数组转换为平衡搜索二叉树
     * @param nums
     * @return
     */
    public TreeNode sortedArrayToBST(int[] nums) {
        if (nums == null || nums.length == 0) return null;

        int start = 0;
        int end = nums.length - 1;
        int rootIndex = start + (end - start) / 2;
        return transform(nums, start, end, rootIndex);
    }

    /**
     * 将数组转换为二叉树
     * @param nums
     * @param start
     * @param end
     * @param rootIndex
     * @return
     */
    private TreeNode transform(int[] nums, int start, int end, int rootIndex) {
        if (rootIndex < start || rootIndex > end) return null;
        TreeNode root = new TreeNode(nums[rootIndex]);
        TreeNode left = transform(nums, start, rootIndex - 1, start + (rootIndex - 1 - start) / 2);
        TreeNode right = transform(nums, rootIndex + 1, end, rootIndex + 1 + (end - rootIndex - 1) / 2);
        root.left = left;
        root.right = right;
        return root;
    }
  • 时间复杂度:O(N)O(N),需要遍历每一个节点
  • 空间复杂度:O(logN)O(\log N),平衡二叉树,递归栈的深度为logN\log N

T538-BST转换为累加树

见LeetCode第538题BST转换为累加树

题目描述

给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。

提醒一下,二叉搜索树满足下列约束条件:

  • 节点的左子树仅包含键 小于 节点键的节点。
  • 节点的右子树仅包含键 大于 节点键的节点。
  • 左右子树也必须是二叉搜索树。

我的思路

  • BST的中序遍历,先遍历右孩子,然后处理当前节点,最后遍历左孩子
  • 有一个 curSum值来记录当前的和
  • 当前节点的值,为curSum + root.val

实现代码


private int curSum = 0;
/**
 * 中序遍历:右 - 中 - 左
 * @param root
 * @param curSum
 * @return
 */
private void traverse(TreeNode root) {

    if (root == null) return ;

    traverse(root.right);

    curSum += root.val;

    root.val = curSum;

    traverse(root.left);

}
    
  • 时间复杂度:O(N)O(N),需要遍历每一个节点
  • 空间复杂度:O(N)O(N),最坏情况下,如果树是一个链表,则递归栈深度为NN