Loading

LeetCode 05 - 二叉树

二叉树迭代遍历

题目链接:
144. 二叉树的前序遍历

94. 二叉树的中序遍历

145. 二叉树的后序遍历

递归和迭代两种方法实质上是一样的,只是递归隐式地维护着一个栈,而迭代则显式地维护一个栈。写递归的时候注意,只需要关注每次需要完成的具体任务,不要纠结递归的细节,把对孩子结点的递归调用当做一个黑箱即可,确信它会完成需要的任务。

写递归函数的三要素:

  • 确定函数参数和返回值:哪些参数是递归过程中需要处理的,就将其作为递归函数的参数;明确函数的返回值。
  • 确定终止条件:如果终止条件不对可能造成 Stack Overflow。
  • 确定当前层递归的任务:将进一步的递归任务当做已知信息,只关注当前层应该做什么。

前序遍历

迭代写法:创建一个栈,首先将根结点入栈,每访问一个结点,就依次将它的右孩子、左孩子加入栈。

public List<Integer> preorderTraverse(TreeNode root) {
    List<Integer> result = new ArrayList<>();
    if (root == null) return result;
    Deque<TreeNode> stack = new LinkedList<>();
    stack.push(root);
    while (!stack.isEmpty()) {
        TreeNode cur = stack.pop();
        result.add(cur.val);
        if (cur.right != null) stack.push(cur.right);
        if (cur.left != null)  stack.push(cur.left);
    }
    return result;
}

中序遍历

// 迭代实现
List<Integer> inorder(TreeNode root) {
    List<Integer> result = new ArrayList<>();
    // 明确维护一个栈
    Deque<TreeNode> stack = new LinkedList<>();
    while(root != null || !stack.isEmpty()) {
        // 走到最左边的结点,同时将途径的结点都入栈
        while(root != null) {
            stack.push(root);
            root = root.left;
        }
        // 此时栈顶为最左的结点
        root = stack.pop();
        result.add(root);
        // 转到右孩子(如果右孩子为空则会继续弹出栈顶结点)
        root = root.right; 
    }
    return result;
}

后续遍历

List<Integer> postorderTraverse(TreeNode root) {
    List<Integer> result = new ArrayList<>();
    if (root == null) return result;
    Deque<TreeNode> stack = new LinkedList<>();
    TreeNode prev = null; // 记录上一次访问的结点,用来判断右子树是否已经访问过
    while (root != null || !stack.isEmpty()) {
        // 走到最左子结点
        while (p != null) {
            stack.push(p);
            p = p.left;
        }
        root = stack.pop();
        if (root.right != null && root.right != prev) { // 右子树还没有访问
            stack.push(root); // 重新入栈
            root = root.right;
        } else { // 右子树已经访问过,则直接访问当前根结点
            result.add(root.val);
            prev = root;
            root = null; // 防止重复往左子树走
        }
    }
    return result;
}

226. 翻转二叉树

二叉树问题的关键点就是 如何将题目要求转化成每个结点应该做的事,这里每个结点要做的就是交换自己的左右孩子:

  • 对于当前 root 结点,交换它的左右孩子 left / right
  • left / right 做同样的操作。
 TreeNode invertTree(TreeNode root) {
     if (root == null) return null; // base case
     // 处理当前 root 结点
     TreeNode temp = root.left;
     root.left = root.right;
     root.right = temp;
     // 递归处理左右孩子
     invertTree(root.left);
     invertTree(root.right);
     return root;
 }

这里是先处理当前节点,再处理左右孩子,是 前序遍历顺序;还可以用 后序遍历 方式来做。

101. 对称二叉树

给定二叉树,判断它是否对称。

分析:可以用 递归迭代 两种方法来做。

递归:如果一个树的左子树与右子树镜像对称,那么称这个树是对称的。而两棵树镜像对称,需要满足两个条件:

  • 两树的根结点值相同;
  • 每棵树的左子树都和另一棵树的右子树对称。(递归关系)

递归思路:

  1. 怎么判断一棵树是不是对称

    • 如果根结点为空,对称;
    • 否则,它的左子树和右子树相互对称,则该树对称。
  2. 怎么判断左树和右树是否相互对称

    • 左树的左孩子和右树的右孩子对称,且左树的右孩子与右树的左孩子对称,则左右子树相互对称。
  3. 定义递归函数 A 用来判断两树是否相互对称,

     def 函数A(左树,右树):
         if 左树.value == 右树.value
             && 函数A(左树.left, 右树.right)
             && 函数A(左树.right, 右树.left):
             return True
    
boolean isSymmetric(TreeNode root) {
    check(root.left, root.right);
}
boolean check(TreeNode left, TreeNode right) {
    // 递归出口
    if(left == null && right == null) return true;
    if(left == null || right == null) return false;
    // 对称条件(两个条件)
    return left.val == q.val && check(left.left, right.right)
                    && check(left.right, right.left);
}

迭代:引入一个队列,是把将递归改写成迭代的常用方式,过程类似于层序遍历。首先将根结点的左右子结点入队,然后在后面的每次迭代中出队两个结点,比较它们的值是否相同,相同则将这两个结点的左右孩子以相反的顺序入队,不相同则说明不对称。

每次出队的两个结点都是在树中处于对称位置的。

boolean check(TreeNode left, TreeNode right) {
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(left);
    queue.offer(right);
    while(!queue.isEmpty()) {
        // 每次出队处于对称位置的两个结点
        TreeNode p = queue.poll();
        TreeNode q = queue.poll();
        // 边界情况
        if(p == null && q == null) continue;
        // 不对称的条件
        if(p == null || q == null || p.val != q.val) return false;
        // p, q 对称则继续向后迭代
        queue.offer(p.left);
        queue.offer(q.right);
        
        queue.offer(p.right);
        queue.offer(q.left);
    }
    return true;
}

111. 二叉树的最小深度

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

输入:
  1 
 / \
2   3
   / \
  4   5

最小深度:2 : 1 → 2

递归:

要素的确定:

  • 返回值:最小深度 —— 一个整数,所以函数签名可以为 int minDepth(TreeNode root)
  • 终止条件if (root == null) return 0;
  • 当前层递归的任务:计算当前子树的最小深度:
    • 如果左子树为空,最小深度为 1 + 右子树的最小深度
    • 如果右子树为空,最小深度为 1 + 左子树的最小深度
    • 如果都不为空,最小深度为 1 + min(左子树最小深度, 右子树最小深度)
public int minDepth(TreeNode root) {
    if (root == null) return 0;
    if (root.left == null && root.right == null) return 1;
    else if (root.left == null) return 1 + minDepth(root.right);
    else if (root.right == null) return 1 + minDepth(root.left);
    else return 1 + Math.min(minDepth(root.left), minDepth(root.right));
}

迭代(使用队列)

public int minDepth(TreeNode root) {
    if (root == null) return 0;
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);
    int depth = 0;
    while (!queue.isEmpty()) {
        int size = queue.size();
        depth++;
        for (int i = 0; i < size; i++) {
            TreeNode cur = queue.poll();
            // 遇到第一个叶子结点,即可返回
            if (cur.left == null && cur.right == null) return depth;
            if (cur.left != null) queue.offer(cur.left);
            if (cur.right != null) queue.offer(cur.right);
        }
    }
    return depth;
}

110. 平衡二叉树

平衡二叉树:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。

方法一:递归(自顶向下)

定义函数 getHeight(node) ,用于计算树的高度,据此可以判断二叉树是否平衡。

自顶向下方式:对于遍历到的当前结点,先计算其左右子树的高度之差是否不大于 1,然后再递归判断左右子树是否为平衡二叉树。

public boolean isBalanced(TreeNode root) {
    // 结束条件
    if (root == null) return true;
    // 当前层任务
    if (Math.abs(getHeight(root.left) - getHeight(root.right)) > 1)
        return false;
    // 进一步递归
    return isBalanced(root.left) && isBalanced(root.right);
}

public int getHeight(TreeNode root) {
    if (root == null) return 0;
    return 1 + Math.max(getHeight(root.left), getHeight(root.right));
}

方法二:递归(自底向上)

自顶向下的递归对每个结点会重复调用 getHeight 函数,自底向上方式可以避免这些重复。

所谓自底向上,就是先从最底层的叶子结点开始实际调用 getHeight 函数。对于当前遍历到的结点,先递归判断左右子树是否都平衡,然后再判断当前数是否平衡。在遍历过程中只要发现一个不平衡的子树,就立即返回这个不平衡的结果。实现方式为,在 getHeight 函数中,如果左右子树不平衡,就将当前树的结果返回为 -1 ,如果平衡就正常返回当前树的高度。

public boolean isBalanced(TreeNode root) {
    return getHeight(root) >= 0;
}

public int getHeight(TreeNode root) {
    if (root == null) return 0; // 结束条件
    int leftHeight = getHeight(root.left);
    if (leftHeight == -1) return -1;
    int rightHeight = getHeight(root.right);
    if (rightHeight == -1) return -1;
    // 当前层任务
    if (Math.abs(leftHeight - rightHeight) > 1)
        return -1; // 不平衡则返回 -1
    return 1 + Math.max(leftHeight, rightHeight);
}

513. 找树左下角的值

找出二叉树最下面那层最左边的结点的值。

方法一:DFS

使用 maxDepthtargetValue 分别记录最左最深结点的深度和结点值。DFS 函数将 curDepth 作为参数,表示当前遍历到的结点的深度,如果 curDepth 大于 maxDepth ,则更新 maxDepth ,并将当前结点作为目标结点。

int maxDepth;
int targetValue;

public int findBottomLeftValue(TreeNode root) {
    if (root == null) return null;
    maxDepth = 0;
    dfs(root, 0);
    return targetValue;
}

public void dfs(TreeNode root, int curDepth) {
    // 终止条件
    if (root == null) return;
    // 当前任务
    curDepth++;
    if (curDepth > maxDepth) {
        maxDepth = curDepth;
        targetValue = root.val;
    }
    // 访问子结点
    dfs(root.left, curDepth);
    dfs(root.right, curDepth); 
}

方法二:BFS

在层序遍历中向队列中放入结点时,先放右孩子,再放左孩子,这样最后访问到的结点就是答案。

public int findBottomLeftValue(TreeNode root) {
    if (root == null) return null;
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);
  int prev = -1;
  while (!queue.isEmpty()) {
    TreeNode cur = queue.poll();
    prev = cur.val;
    if (cur.right != null) {
      queue.offer(cur.right);
    }
    if (cur.left != null) {
      queue.offer(cur.left);
    } 
  }
  return prev;
}

257. 二叉树的所有路径

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

方法:DFS

DFS 三要素:

  • 结束条件:到达叶结点,将当前路径加入结果集;
  • 当前层任务:将当前结点加入路径;
  • 访问子结点:对左右孩子调用 DFS 函数。
List<String> result;
public List<String> binaryTreePaths(TreeNode root) {
  result = new ArrayList<>();
  if (root == null) return result;
  StringBuilder track = new StringBuilder();
  dfs(root, track);
  return result;
}

void dfs(TreeNode root, StringBuilder track) {
  // 终止条件
  if (root.left == null && root.right == null) {
    track.append(root.val);
    result.add(track.toString());
    return;
  }
  // 当前任务
  track.append(root.val);
  track.append("->");
  // 访问子结点
  if (root.left != null) dfs(root.left, new StringBuilder(track));
  if (root.right != null) dfs(root.right, new StringBuilder(track));
}

404. 左叶子之和

方法一:递归

三要素:

  • 返回值:当前树中左叶子之和。
  • 结束条件:当前树为空,则返回 0。
  • 当前层任务:如果当前树根结点的左孩子是叶结点,则记录其值,累加到左子树和右子树的递归调用结果上。
int sumOfLeftLeaves(TreeNode root) {
    // 结束条件
    if (root == null) return 0;
    // 当前层任务
    TreeNode left = root.left;
    int curValue = 0;
    if (left != null && left.left == null && left.right == null)
        curValue = left.val;
    // 进一步递归
    int leftResult = sumOfLeftLeaves(root.left);
    int rightResult = sumOfLeftLeaves(root.right);
    return curValue + leftResult + rightResult;
}

DFS 写法:

int sumOfLeftLeaves(TreeNode root) {
    if (root == null) return 0;
    return dfs(root);
}

int dfs(TreeNode root) {
    int sum = 0;
    TreeNode left = root.left;
    if (left != null) {
        if (left.left == null && left.right == null)
            sum += left.val;
        else
            sum += dfs(left);
    }
    TreeNode right = root.right;
    if (right != null && (right.left != null || right.right != null)) {
        sum += dfs(right);
    }
    return sum;
}

112. 路径总和

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

方法:递归

  • 函数的功能为:查看是否存在从当前结点到叶结点的路径,其总和为 sum
  • 假定从根节点到当前节点的值之和为 val,我们可以将这个大问题转化为一个小问题:是否存在从当前节点的子节点到叶子的路径,满足其路径和为 sum - val
  • 不难发现这满足递归的性质,若当前节点就是叶子节点,那么我们直接判断 sum 是否等于 val 即可,否则我们只需要递归地询问它的子节点是否能满足条件即可。
boolean hasPathSum(TreeNode root, int sum) {
    if (root == null) return false;
    if (root.left == null && root.right == null) {
        return root.val == sum;
    }
    return hasPathSum(root.left, sum - root.val) 
                || hasPathSum(root.right, sum - root.val);
}

654. 最大二叉树

给定一个不含重复元素的整数数组 nums。一个以此数组直接递归构建的 最大二叉树 定义如下:

  • 二叉树的根是数组 nums 中的最大元素。
  • 左子树是通过数组中 最大值左边部分 递归构造出的最大二叉树。
  • 右子树是通过数组中 最大值右边部分 递归构造出的最大二叉树。

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

先找到最大值所在索引,构造根结点 root,然后对左边数组和右边数组递归调用,作为 root 的左右子树。

 public TreeNode constructMaximumBinaryTree(int[] nums) {
     // base case
     if (nums.length == 0) return null;
     int length = nums.length;
     int maxIndex = maxIdx(nums);
     // 构造根结点
     TreeNode root = new TreeNode(nums[maxIndex]);
     // 递归构造左右孩子
     int[] leftPart = new int[maxIndex];
     int[] rightPart = new int[nums.length - maxIndex - 1];
     for (int i = 0; i < maxIndex; i++)
         leftPart[i] = nums[i];
     for (int i = maxIndex+1, j = 0; i < nums.length; i++, j++)
         rightPart[j] = nums[i];
     root.left = constructMaximumBinaryTree(leftPart);
     root.right = constructMaximumBinaryTree(rightPart);
     return root;
 }
 
 int maxIdx(int[] nums) {
     int maxIndex = 0;
     for (int i = 1; i < nums.length; i++)
         if (nums[i] > nums[maxIndex])
             maxIndex = i;
 
     return maxIndex;
 }

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

分析:preorderinorder 中的元素分布有下面的特点:

每次从 前序遍历 能得到的信息是根结点,然后利用发现的根结点在 中序遍历 序列中能够找出 左右子树 包含的结点。再递归地从前序遍历中找到子树的根结点,循环往复。

为了在每次递归中确定当前子树的两个序列,需要知道这棵树在前序序列中的开始和结束位置,以及在中序序列中的开始和结束位置。

TreeNode buildTree(int[] preorder, int[] inorder) {
    return build(preorder, 0, preorder.length-1,
                 inorder, 0, inorder.length-1);
}

TreeNode build(int[] preorder, int preStart, int preEnd,
               int[] inorder, int inStart, int inEnd) {
    // base case
    if (preStart > preEnd) return null;
    // 构造根结点
    int rootValue = preorder[preStart];
    TreeNode root = new TreeNode(rootValue);
    // 在中序遍历序列中找到根结点
    int rootIndex = 0;
    for (int i = inStart; i <= inEnd; i++) {
        if (inorder[i] == rootValue) {
            rootIndex = i;
            break;
        }
    }
    // 构造左右子树(关键是在两个序列中找出左右子树的索引范围)
    int leftSize = rootIndex - inStart;
    root.left = build(preorder, preStart + 1, preStart + leftSize,
                      inorder, inStart, rootIndex - 1);
    root.right = build(preorder, preStart + leftSize + 1, preEnd,
                       inorder, rootIndex + 1, inEnd);
    // 返回当前子树的根结点
    return root;
}

652. 寻找重复的子树

给定一棵二叉树,返回所有重复的子树。对于同一类的重复子树,你只需要返回其中任意一棵的根结点即可。两棵树重复是指它们具有相同的结构以及相同的结点值

需要明确两个问题:

  • 以自己为根结点的这棵子树长什么样?
  • 以其他结点为根结点的子树长什么样?

对于第一个问题,怎么描述一棵子树?可以通过拼接字符串的方式把 二叉树序列化,例如使用子树的后序遍历序列标识该子树,用特殊符号,例如 # 表示空节点。

对于第二个问题,这里使用 HashMap 记录每种子树的序列及其出现次数,遍历每个结点时,查看当前序列在其中出现的次数,如果是 1,则将该序列加入结果集,随后该序列的次数会加一。这样保证了重复子树只会被加入一次。

 HashMap<String, Integer> memo = new HashMap<>(); // 所有子树及其次数
 LinkedList<TreeNode> result = new LinkedList<>(); // 重复子树的根结点
 
 List<TreeNode> findDuplicateSubtrees(TreeNode root) {
     traverse(root);
     return result;
 }
 
 String traverse(TreeNode root) {
     // 求出表示序列(后序遍历顺序)
     if (root == null) return "#";
     String left = traverse(root.left);
     String right = traverse(root.right);
     String subTree = left + "," + right + "," + root.val;
     // 检查是否重复
     int count = memo.getOrDefault(subTree, 0);
     if (count == 1) result.add(subTree); // 发现重复只会加入一次
     memo.put(subTree, count + 1); // 更新 subTree 的出现次数

     return subTree;
 }

297. 二叉树的序列化与反序列化

可以用前序遍历、后序遍历或层序遍历的方法来序列化和执行相应的反序列化,但不能用中序遍历,因为中序遍历不知道根结点位置。

前序遍历方法

String SEP = ",";
String NULL = "#";
// 序列化
String serialize(TreeNode root) {
    StringBuilder builder = new StirngBuilder();
    serialize(root, builder);
    return builder.toString();
}
void serialize(TreeNode root, StringBuilder builder) {
    // 空节点
    if (root == null) {
        builder.append(NULL).append(SEP);
        return;
    }
    // 处理根结点
    builder.append(root.val).append(SEP);
    serialize(root.left, builder);
    serialize(root.right, builder);
}
// 反序列化
TreeNode deserialize(String data) {
    // 将序列转化成列表
    LinkedList<String> nodes = new LinkedList<>();
    for (String s : data.split(",")) nodes.addLast(s);
    return deserializeAux(nodes);
}
TreeNode deserializeAux(LinkedList<String> nodes) {
    if (nodes.isEmpty()) return null;
    // 构造根结点
    String first = nodes.removeFirst(); // 取出第一个结点
    if (first.equals(NULL)) return null;
    TreeNode root = new TreeNode(Integer.parseInt(first));
    // 构造左右孩子
    root.left = deserializeAux(nodes);
    root.right = deserializeAux(nodes);
    
    return root;
}

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

如果是满二叉树,结点总数可以通过公式直接计算:2h1. 利用这一点,在计算每个子树的结点个数时,先判断是否为满二叉树,如果是则直接计算,否则按照普通二叉树的方法递归计算。

int countNodes(TreeNode root) {
    if (root == null) return 0;
    TreeNode l = root, right = root;
    int mostLeftHeight = 0, mostRightHeight = 0;
    while (l != null) {
        l = l.left;
        mostLeftHeight++;
    }
    while (r != null) {
        r = r.right;
        mostRightHeight++;
    }
    if (mostLeftHeight == mostRightHeight)
        return (int) Math.pow(2, mostLeftHeight) - 1;

    return 1 + countNodes(root.left) + countNodes(root.right);
}

230. 二叉搜索树中第K小的元素

中序遍历,访问到的第 k 个元素就是要求的元素。

int findKthSmallest(TreeNode root, int k) {
  traverse(root, k);
  return result;
}
int counter = 1, result;
void traverse(TreeNode root, int k) {
    if (root == null) return;
    // 左
    traverse(root.left, k);
    // 根
    if (counter == k) result = root.val;
    counter++;
    // 右
    traverse(root.right, k);
}

上面是用递归方法写的,下面是用栈迭代实现的:

int findKthSmallest(TreeNode root, int k) {
    Deque<TreeNode> stack = new LinkedList<>();
    while (!stack.isEmpty() || root != null) {
        while(root != null) {
            stack.push(root);
            root = root.left;
        }
        root = stack.pop();
        --k;
        if (k == 0) break;
        root = root.right;
    }
    return root.val;
}

617. 合并二叉树

给定两棵二叉树,合并规则是:对应相同位置结点都存在,则值相加;否则不为空的结点作为新二叉树的结点。

方法一:递归(DFS)

TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
    // 递归出口
    if(root1 == null) return root2;
    if(root2 == null) return root1;
    // 处理根结点
    TreeNode root = new TreeNode(root1.val + root2.val);
    // 递归合并左右孩子
    root.left = mergeTrees(root1.left, root2.left);
    root.right = mergeTrees(root1.right, root2.right);
    return root;
}

方法二:迭代(BFS)

使用三个队列分别存储合并后的树结点(queue)、原始的两个树的结点 (queue1, queue2)。每一轮迭代中同时从 queue1, queue2 中出队两个元素,查看这两个结点的左右孩子是否为空,根据不同情况创建新节点后放入 queue

TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
    if(root1 == null) return root2;
    if(root2 == null) return root1;
    Queue<TreeNode> queue = new LinkedList<>();
    Queue<TreeNode> queue1 = new LinkedList<>();
    Queue<TreeNode> queue2 = new LinkedList<>();
    queue1.offer(root1);
    queue2.offer(root2);
    queue.offer(new TreeNode(root1.val + root2.val));
    // 开始迭代
    while(!queue1.isEmpty() && !queue2.isEmpty()) {
        TreeNode node = queue.poll(), node1 = queue1.poll(), 
                                        node2 = queue2.poll();
        TreeNode left1 = node1.left, right1 = node1.right;
        TreeNode left2 = node2.left, right2 = node2.right;
        // 检查左孩子
        if(left1 != null || left2 != null) {
            // 都不为空
            if(left1 != null && left2 != null) {
                TreeNode left = new TreeNode(left1.val+left2.val);
                node.left = left;
                queue1.offer(left1);
                queue2.offer(left2);
                queue.offer(left);
            }
            // 一个为空则直接将另一个作为合并后的结点,后续迭代停止
            else if(left1 != null) node.left = left1;
            else if(left2 != null) node.left = left2;
        }
        // 以同样方式检查右孩子
        if(right1 != null || right2 != null) {
            // ...
        }
    }
    return root;
}

572. 另一棵树的子树

给定两个二叉树 s,t,判断第二个是否是第一个的子树。

方法:DFS 暴力匹配

先判断 subtree 是否为根结点的子树,如果不是则判断是否为 root.left, root.right 的子树。

boolean isSubtree(TreeNode s, TreeNode t) {
    if (s == null) return false;
    if (check(s, t)) return true;
    return isSubtree(s.left, t) || isSubtree(s.right, t);
}
// 判断两棵树是否相同
boolean check(TreeNode s, TreeNode t) {
    if(s == null && t == null)
        return true;
    else if (s == null || t == null || s.val != t.val) 
        return false;
    return check(s.left, t.left) && check(s.right, t.right);
}

543. 二叉树的直径

直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。

方法:递归

对于一个结点 root ,以它为根的子树的直径root.leftroot.right深度之和。在递归计算每个结点的直径时,不断更新直径的最大值,最后将其返回即可。

int maxDiameter = 0;
int diameterOfBinaryTree(TreeNode root) {
    depth(root);
    return maxDiameter;
}

int depth(TreeNode root) {
    // 递归结束条件
    if(root == null) return 0;
    // 根据左右子树的深度确定当前子树的直径
    int leftDepth = depth(root.left),
            rightDepth = depth(root.right);
    int diameter = leftDepth + rightDepth;
    // 更新最大直径
    maxDiameter = Math.max(maxDiameter, diameter);
    // 返回当前子树的深度
    return Math.max(leftDepth, rightDepth) + 1;
}

116. 填充每个节点的下一个右侧节点指针

给定二叉树,每个结点的 next 指针应该指向同一层的右边一个结点,但初始状态下都为空,请填充这些指针。

方法一:递归(适用于完全二叉树)

递归任务定义为:连接两个相邻结点(node1.next=node2),同时连接每个结点的左右孩子结点,以及第一个结点的右孩子和第二个结点的左孩子。

Node connect(Node root){
    if(root == null) return null;
    connectTwoNodes(root.left, root.right);
    return root;
}

void connectTwoNodes(Node node1, Node node2) {
    if(node1 == null || node2 == null) return;
    node1.next = node2;
    connectTwoNodes(node1.left, node2.left);
    connectTwoNodes(node2.left, node2.right);
    connectTwoNodes(node1.right, node2.left);
}

方法二:迭代(BFS)

每次处理一层结点。

Node connect(Node root) {
    if(root == null) return null;
    Queue<Node> queue = new LinkedList<>();
    queue.offer(root);
    while(!queue.isEmpty()) {
        // 每轮迭代处理一层
        int size = queue.size();
        for(int i = 0; i < size; i++) {
            Node node = queue.poll();
            // 只填充前 size-1 个结点
            if(i < size-1) node.next = queue.peek();
            // 下一层结点入队
            if(node.left != null) queue.offer(node.left);
            if(node.right != null) queue.offer(node.right);
        }
    }
    return root;
}

方法三:使用当前层已经填充的 next 指针建立下一层的 next 关系

这个方法适用于完全二叉树。

两个相邻结点如果互为兄弟,容易建立 next 关系: node.left.next = node.right ;如果在不同的父结点之下,基于当前层已经填充的 next 指针,可以这样建立下一层的 next 关系: node.right.next = node.next.left

也就是说,遍历到第 N 层时,第 N 层已经在上一轮迭代中填充好了 next 指针,当前的目的是建立下一层,即第 N+1 层的 next 关系。

每一层的连接需要借助一个指针来串联,这个指针首先指向最左边结点,然后不断右移,连接完成时,它又指向下一层的最左边结点。

Node connect(Node root) {
    if(root == null) return null;
    Node leftMostNode = root; // 用它来表示每一层迭代开始时的指针指向
    // 注意循环条件,迭代到倒数第二层即可
    while(leftMostNode.left != null) {

        Node p = leftMostNode;
        // 注意这里的 p != null 和下面的 p.next != null
        while(p != null) {
            p.left.next = p.right; // 第一种情况
            if(p.next != null)  // 第二种情况
                p.right.next = p.next.left;
            p = p.next;
        }
        // 准备下一层的迭代
        leftMostNode = leftMostNode.left;
    }

    return root;
}

117. 填充每个节点的下一个右侧节点指针 II

方法一:层序遍历

Node connect(Node root) {
    if(root == null) return null;
    Queue<Node> queue = new LinkedList<>();
    queue.offer(root);
    while(!queue.isEmpty()) {
        int size = queue.size();
        Node last = null; // 引线的针
        for(int i = 0; i < size; i++) {
            Node cur = queue.poll();
            // 连接
            if(i == 0) last = cur;
            else {
                last.next = cur;
                last = last.next;
            }
            // 下一层入队
            if(cur.left != null) queue.offer(cur.left);
            if(cur.right != null) queue.offer(cur.right);
        }
    }
    return root;
}

方法二:借助上一层的连接创建当前层的连接

Node connect(Node root) {
    if(root == null) return null;
    Node start = root;
    while(start != null) {
        // 每一层开始时,last 和 nextStart 需要初始化
        Node last = null; // 引线的针
        Node nextStart = null;  // 每一层的开头结点
        for(Node p = start; p != null; p = p.next) {
            // 连接 p.child 那一层
            if(p.left != null) {
                // 连接
                if(last == null) 
                    last = p.left;
                else {
                    last.next = p.left;
                    last = last.next;
                }
                // 记录新的开始结点,以供下一层连接
                if(nextStart == null)
                     nextStart = p.left;
            }
            if(p.right != null) {
                if(last == null) 
                    last = p.right;
                else {
                    last.next = p.right;
                    last = last.next;
                }
                if(nextStart == null) 
                    nextStart = p.right;
            }
        } // end of for p
        start = nextStart;
    }
    return root;
}

98. 验证二叉搜索树

判断一棵树二叉树是否是二叉搜索树。

方法一:递归

递归函数 helper(root, low, high) 表示判断以 root 为根的子树中的所有结点是否都在 (low, high) 范围内。如果在该范围内,则递归判断左子树:helper(root.left, low, root.val) ,以及右子树:helper(root.right, root.val, high)

boolean isValidBST(TreeNode root) {
    return helper(root, Long.MIN_VALUE, Long.MAX_VALUE);
}

boolean helper(TreeNode root, int low, int high) {
    if(root == null) return true;
    if(root.val <= low || root.val >= high) return false;
    return helper(root.left, low, root.val)
                    && helper(root.right, root.val, high);
}

方法二:中序遍历

递归版本

long pre = Long.MIN_VALUE;

boolean isValidBST(TreeNode root) {
    return inorder(root);
}

boolean inorder(TreeNode root) {
    if(root == null) return true;
    // 左子树
    boolean left = inorder(root.left);
    // 根结点
    if(pre >= root.val) return false;    
    pre = root.val;
    // 右子树
    boolean right = inorder(root.right);
    return left && right;
}

栈版本

boolean isValidBST(TreeNode root) {
    Deque<TreeNode> stack = new LinkedList<>();
    double pre = Long.MIN_VALUE;
    while(!stack.isEmpty() || root != null) {
        // 走到最左边
        while(root != null) {
            stack.push(root);
            root = root.left;
        }
        root = stack.pop();
        // 访问根结点
        if(root.val <= pre) return false;
        pre = root.val;
        // 转到右子树
        root = root.right; 
    }
    return true;
}

501. 二叉搜索树中的众数

找到出现频率最高的所有元素

BST 的中序遍历序列是一个有序数组,所以相同的数字是相连的,那么:

  • pre 指针指向前一个访问的结点,初始时 pre==null
  • 使用 count 来记录当前遍历到的数字已经出现了几次;
  • maxCount 记录遍历过的数字中最大的次数。
  • 用一个 List 对象记录所有众数。

遍历到一个结点时:

  • 如果 pre == null ,说明当前结点是第一个结点,将 count 设为 1
  • 如果 pre.val != root.val ,说明当前结点第一次出现,将 count 设为 1
  • 如果 pre.val == root.val ,说明当前结点值再次出现,count++

这时检查 countmaxCount 的大小关系:

  • 如果 count == maxCount ,将当前结点值也加入结果列表。
  • 如果 count > maxCount ,则更新 maxCount ,同时将结果列表清空,然后将当前结点值加入结果列表,因为之前记录的值都不再是最大频次结点了。
TreeNode pre;
int count;
int maxCount;
List<Integer> list;

public int[] findMode(TreeNode root) {
    pre = null;
    count = 0;
    maxCount = 0;
    list = new ArrayList<>();
    inorder(root);
    int[] result = new int[list.size()];
    int i = 0;
    for (int value : list)
        result[i++] = value;
    return result;
}

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

    inorder(root.left);

    int rootVal = root.val;
    if (pre == null || pre.val != rootVal) {
        count = 1;
    } else {
        count++;
    }
    pre = root;
    if (count > maxCount) {
        list.clear();
        list.add(rootVal);
        maxCount = count;
    }
    else if (count == maxCount) {
        list.add(rootVal);
    }

    inorder(root.right);
}

二叉树的最近公共祖先

方法一:递归

TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    if(root == null) return null;
    // 如果 p,q 有一个是根结点,则lca是根结点
    if(root.val==p.val || root.val==q.val) return root;
    // 如果 p,q 都在左子树,则到左子树递归查找
    if(isExist(root.left, p) && isExist(root.left, q))
        return lowestCommonAncestor(root.left, p, q);
    // 如果 p,q 都在右子树,则到右子树递归查找
    if(isExist(root.right, p) && isExist(root.right, q))
        return lowestCommonAncestor(root.right, p, q);
    // 如果 p,q 分别属于左右子树,则lca是根结点
    return root;
}

boolean isExist(TreeNode root, TreeNode node) {
    if(root == null) return false;
    if(root.val == node.val) return true;
    boolean left = isExist(root.left, node);
    boolean right = isExist(root.right, node);
    return left || right;
}

方法二:记录每个结点的父结点

算法过程如下:

  1. 从根结点开始遍历二叉树,用哈希表记录每个结点的父结点
  2. p 结点开始向上记录它的各级祖先节点 (记为 visited 集合)。
  3. 再从 q 结点开始向上遍历各级祖先节点,如果这个祖先节点存在于 visited 中,则说明这个结点就是答案。
Map<Integer, TreeNode> node_parent = new HashMap<>();
Set<Integer> visited = new HashSet<>();
// 记录各个结点的父结点
void dfs(TreeNode root) {
    if(root.left != null) {
        node_parent.put(root.left.val, root);
        dfs(root.left);
    }
    if(root.right != null) {
        node_parent.put(root.right.val, root);
        dfs(root.right);
    }
}

TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    dfs(root);
    while(p != null) {
        visited.add(p.val);
        p = node_parent.get(p.val); // 向上溯源
    }
    while(q != null) {
        if(visited.contains(q.val)) 
            return q;
        q = node_parent.get(q.val); // 溯源
    }
    return null;
}

450. 删除二叉搜索树中的节点

找到要删除的结点 root 后,分几种情况:

  • 没有孩子结点,直接删除 root ,返回 null
  • 只有左孩子 / 右孩子,则将左孩子 / 右孩子作为新的子树,直接返回。
  • 有左右孩子,可以将右子树的最小结点(中序遍历中 root 的后继结点)提到 root 位置,然后递归地将这个结点从右子树中删除。
public TreeNode deleteNode(TreeNode root, int key) {
    if (root == null) return null;
    if (root.val > key) {
        root.left = deleteNode(root.left, key);
        return root;
    }
    if (root.val < key) {
        root.right = deleteNode(root.right, key);
        return root;
    }
    else {
        if (root.left == null && root.right == null) return null;
        if (root.left == null) return root.right;
        if (root.right == null) return root.left;
        TreeNode rightMin = root.right;
        while (rightMin.left != null) rightMin = rightMin.left;
        root.right = deleteNode(root.right, rightMin.val);
        rightMin.right = root.right;
        rightMin.left = root.left;
        return rightMin;
    }
}

669. 修剪二叉搜索树

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

方法:递归

要素确定:

  • 返回值:删除不在范围内的所有结点后,返回根结点。
  • 终止条件:root==null ,直接返回 null
  • 当前任务:
    • 如果 root.val < low ,说明 root 及其左子树都需要删除,而其右子树需要递归修剪,并将修剪的结果返回,替代 root
    • 如果 root.val > high ,说明 root 及其右子树都需要删除,而其左子树需要递归修剪,并将修剪的结果返回,替代 root
    • 否则,递归修剪其左右子树。
public TreeNode trimBST(TreeNode root, int low, int high) {
    if (root == null) return null;
    if (root.val < low) {
        root = trimBST(root.right, low, high);
    } else if (root.val > high) {
        root = trimBST(root.left, low, high);
    } else {
        root.left = trimBST(root.left, low, high);
        root.right = trimBST(root.right, low, high);
    }
    return root;
}

二叉树展开为单链表

链表顺序为二叉树前序遍历的顺序。

方法一:前序遍历

void preorderTraverse(TreeNode root, List<TreeNode> list) {
    if(root == null) return;
    list.add(root);
    preorderTraverse(root.left, list);
    preorderTraverse(root.right, list);
}

void flatten(TreeNode root) {
    List<TreeNode> list = new ArrayList<>();
    preorderTraverse(root, list);
    // 建立前序序列结点的连接
    for(int i = 1; i < list.size(); i++) {
        list.get(i-1).left = null;
        list.get(i-1).right = list.get(i);
    }
}

方法二:前序遍历和展开同时进行

方法一是将前序遍历和展开分成了两步,这里将其合而为一,但因为涉及到前序遍历的内部细节,不能使用递归方式来实现前序遍历,而要使用迭代方式:在每一轮迭代中,将 上一个访问的结点 prev 的右孩子设为当前节点 cur ,然后将 prev 设为 cur ,继续遍历。

void flatten(TreeNode root) {
    if(root == null) return;
    Deque<TreeNode> stack = new LinkedList<>();
    stack.push(root);
    TreeNode prev = null;
    while(!stack.isEmpty()) {
        TreeNode cur = stack.pop();
        if(prev != null) {
            prev.left = null;
            prev.right = cur;
        }
        if(cur.right != null)
            stack.push(cur.right);
        if(cur.left != null)
            stack.push(cur.left);
        prev = cur;
    }
}

方法三:递归

先递归处理左右子树,然后把左子树放到右子树位置,最后把右子树作为左子树的右孩子。

void flatten(TreeNode root) {
    if(root == null) return;
    // 递归处理左右子树
    flatten(root.left);
    flatten(root.right);
    // 处理当前根结点
    TreeNode right = root.right;
    TreeNode left = root.left;
    root.left = null;
    root.right = left; // 左子树变成右子树
    TreeNode p = root;
    while(p.right != null)
        p = p.right;
    p.right = right; // 右子树变成原来左子树的右子树
}

538 把二叉搜索树转换为累加树

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

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

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

方法:反序中序遍历

按照 右孩子 -> 根结点 -> 左孩子 的顺序遍历,实时更新遍历过的结点之和。

 TreeNode covertBST(TreeNode root) {
     traverse(root);
     return root;
 }
 
 int sum = 0;
 void traverse(TreeNode root) {
     if(root == null) return;
     traverse(root.right);
     sum += root.val;
     root.val = sum;
     traverse(root.left);
 }

二叉搜索树常用操作

搜索一个元素

boolean isInBST(TreeNode root, int target) {
    if (root == null) return false;
    if (root.val == target) return true;
    else if (root.val > target) return isInBST(root.left, target);
    else return isInBST(root.right, target);
}

插入一个元素

TreeNode insert(TreeNode root, int target) {
    if (root == null) return new TreeNode(target);
    if (root.val > target) 
        root.left = insert(root.left, target);
    if (root.val < target)
        root.right = insert(root.right, target);
    return root;
}

删除一个结点

待删除结点有三种情况:

  • 没有左孩子:用右孩子替代自己。
  • 没有右孩子:用左孩子替代自己。
  • 有左右孩子:用左子树的最大值 / 右子树的最小值替代自己。
TreeNode deleteNode(TreeNode root, int target) {
    if (root == null) return null;
    if (root.val == target) { // 找到待删除结点
        if (root.left == null) return root.right;
        if (root.right == null) return root.left;
        TreeNode minNode = getMinNode(root.right);
        // 用右子树的最小值替代自己
        root.val = minNode.val;
        root.right = deleteNode(root.right, minNode.val);
    } else if (root.val > target) {
        root.left = deleteNode(root.left, target);
    } else {
        root.right = deleteNode(root.right, target);
    }
}

TreeNode getMinNode(TreeNode root) {
    if (root == null) return null;
    TreeNode p = root;
    while (p.left != null) {
        p = p.left;
    }
    return p;
}
posted @   李志航  阅读(42)  评论(0编辑  收藏  举报
努力加载评论中...
点击右上角即可分享
微信分享提示