LeetCode - 6. 二叉树(二)

刷题顺序来自:代码随想录

左叶子之和

404. 左叶子之和

计算给定二叉树的所有左叶子之和。

private int sum = 0;
public int sumOfLeftLeaves(TreeNode root) {
    if(root == null) {
        return sum;
    }
    if(root != null & root.left != null && root.left.left == null && root.left.right == null) {
        sum += root.left.val;
    }
    sumOfLeftLeaves(root.left);
    sumOfLeftLeaves(root.right);

    return sum;
}

找树左下角的值

513. 找树左下角的值

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

迭代法

层级遍历,每次记录第一个节点的值。

递归法

遍历整颗树,每次遇到叶子节点,表示需要更新深度。记录第一个更新深度叶子节点的值。

int depth = 0;
int left = 0;

public int findBottomLeftValue(TreeNode root) {
    findBottomLeft(root, 1);
    return left;
}

private void findBottomLeft(TreeNode root, int currDepth) {
    if(root == null) {
        return;
    }
    // 如果是叶子节点,递归终止,查看是否要更新深度
    if(root.left == null && root.right == null) {
        // 如果需要更新,这表明是当前层遇到的第一个叶子节点(最左侧),则记录下它的值
        if(depth < currDepth) {
            depth = currDepth;
            left = root.val;
        }
        return;
    }
    findBottomLeft(root.left, currDepth + 1);
    findBottomLeft(root.right, currDepth + 1);
}

路径总和

112. 路径总和

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

boolean found = false;  // 记录是否找到路径
public boolean hasPathSum(TreeNode root, int targetSum) {
    pathSum(root, targetSum, 0);
    return found;
}

// currSum记录当前节点之前路径的总和
private void pathSum(TreeNode root, int targetSum, int currSum) {
    // 递归终止条件:当前节点为null,或者已经找到了一条正确的路径
    if(root == null || found) return;

    currSum += root.val;  // 更新当前路径总和
    if(root.left == null && root.right == null && targetSum == currSum) {
        found = true;
        return;
    }
	
    // 遍历整颗树
    pathSum(root.left, targetSum, currSum);
    pathSum(root.right, targetSum, currSum);
}

113. 路径总和 II

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

和前面的方法类似:

ArrayList<List<Integer>> res = new ArrayList<>();  // 结果

public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
    findPath(root, targetSum, new ArrayList<Integer>());
    return res;
}

private void findPath(TreeNode root, int targetSum, List<Integer> path) {
    if(root == null) return;
	
    path.add(root.val);  // 更新当前路径
    if(root.left == null && root.right == null && targetSum == root.val) {
        res.add(path);
        return;
    }
	
    // 遍历整颗树
    findPath(root.left, targetSum - root.val, new ArrayList<>(path));  // 深拷贝
    findPath(root.right, targetSum - root.val, new ArrayList<>(path));
}

回溯

上面的方法速度慢,占用内存空间大的原因是:复制了多个path数组。使用回溯法,只用一个数组表示路径,在经过当前节点之后,将节点从数组中移除。

ArrayList<List<Integer>> res =  new ArrayList<>();
ArrayList<Integer> path = new ArrayList<>();
public List<List<Integer>> pathSum (TreeNode root,int targetSum) {
    travesal(root, targetSum);
    return res;
}

private void travesal(TreeNode root,  int count) {
    if(root == null) {
        return;
    }

    path.add(root.val);  // 当前节点加入路径
    if(root.left == null && root.right == null && count == root.val) {
        res.add(new ArrayList<>(path));  // 深拷贝
        // return;  // 注意这里不能return,因为path add的数据没有移除
    }

    travesal(root.left, count - root.val);
    travesal(root.right, count - root.val);

    path.remove(path.size()-1);  // 回溯,当前节点移除出路径
}

由遍历结果构造二叉树

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

根据一棵树的中序遍历与后序遍历构造二叉树。你可以假设树中没有重复的元素。

public TreeNode buildTree(int[] inorder, int[] postorder) {
    TreeNode root = new TreeNode(postorder[postorder.length-1]);
    build(inorder, postorder, root, 0, inorder.length, 0, postorder.length);
    return root;
}

// 每次递归,建立root节点的左右儿子节点
// left和right指示了遍历结果的位置,左闭右开
private TreeNode build(int[] inorder, int[] postorder, TreeNode root, int inLeft, int inRight, int postLeft, int postRight) {
    if(inRight - inLeft <= 1) return root;
	
    // 找到root节点在中序遍历结果的索引
    int rootIndex = inLeft;
    for(int i = inLeft; i < inRight; i++) {
        if(root.val == inorder[i]) {
            rootIndex = i;
            break;
        }
    }
	
    // 建立儿子节点前,首先判断儿子节点是否存在
    if(rootIndex != inLeft) {
        root.left = new TreeNode(postorder[postLeft+rootIndex-inLeft-1]);
        build(inorder, postorder, root.left, inLeft, rootIndex, postLeft, postLeft+rootIndex-inLeft);
    }
    if(rootIndex != inRight-1) {
        root.right = new TreeNode(postorder[postRight-2]);
        build(inorder, postorder, root.right, rootIndex+1, inRight, postLeft+rootIndex-inLeft, postRight-1);
    }
	
    return root;
}

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

给定一棵树的前序遍历 preorder 与中序遍历 inorder。请构造二叉树并返回其根节点。

与上一题类似:

public TreeNode buildTree(int[] preorder, int[] inorder) {
    TreeNode root = new TreeNode(preorder[0]);
    build(preorder, inorder, root, 0, preorder.length, 0, inorder.length);
    return root;
}

private TreeNode build(int[] preorder, int[] inorder, TreeNode root, int preLeft, int preRight, int inLeft, int inRight) {
    if(preRight-preLeft <= 1) return root;

    // 找到root节点在中序遍历的索引
    int rootIndex = inLeft;
    for(int i = inLeft; i < inRight; i++) {
        if(root.val == inorder[i]) {
            rootIndex = i;
            break;
        }
    }

    if(rootIndex != inLeft) {
        root.left = new TreeNode(preorder[preLeft+1]);
        build(preorder, inorder, root.left, preLeft+1, preLeft+rootIndex-inLeft+1, inLeft, rootIndex);
    }
    if(rootIndex != inRight - 1) {
        root.right = new TreeNode(preorder[preLeft+rootIndex-inLeft+1]);
        build(preorder, inorder, root.right, preLeft+rootIndex-inLeft+1, preRight, rootIndex+1, inRight);
    }

    return root;
}

最大二叉树

654. 最大二叉树

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

  1. 二叉树的根是数组 nums 中的最大元素。
  2. 左子树是通过数组中 最大值左边部分 递归构造出的最大二叉树。
  3. 右子树是通过数组中 最大值右边部分 递归构造出的最大二叉树。
public TreeNode constructMaximumBinaryTree(int[] nums) {
    return construct(nums, 0, nums.length);
}

// 每次递归,构建最大值为根节点
private TreeNode construct(int[] nums, int left, int right) {
    if(right <= left) return null;

    // 找出最大值的索引
    int maxIndex = left;
    for(int i = left; i < right; i++) {
        if(nums[i] > nums[maxIndex]) {
            maxIndex = i;
        }
    }

    TreeNode root = new TreeNode(nums[maxIndex]);
    root.left = construct(nums, left, maxIndex);
    root.right = construct(nums, maxIndex+1, right);

    return root;
}

合并二叉树

617. 合并二叉树

给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。

public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
    if(root1 == null) {
        return root2;
    }
    if(root2 == null) {
        return root1;
    }
    root1.val += root2.val;
    root1.left = mergeTrees(root1.left, root2.left);
    root1.right = mergeTrees(root1.right, root2.right);
    return root1;
}

二叉搜索树

700. 二叉搜索树中的搜索

给定二叉搜索树(BST)的根节点和一个值。 你需要在BST中找到节点值等于给定值的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 NULL。

public TreeNode searchBST(TreeNode root, int val) {
    if(root == null || root.val == val) return root;
    if(root.val < val) return searchBST(root.right, val);
    else return searchBST(root.left, val);
}

98. 验证二叉搜索树

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

ArrayList<Integer> list;
public boolean isValidBST(TreeNode root) {
    list = new ArrayList<Integer>();
    inorder(root);
    return isOrder(list);
}

// 中序遍历,结果存储到list
private void inorder(TreeNode root) {
    if(root != null) {
        inorder(root.left);
        list.add(root.val);
        inorder(root.right);
    }
}

// 判断list是否是单调递增的数组
private boolean isOrder(ArrayList<Integer> list) {
    if(list.size() <= 1) return true;
    for(int i = 1; i < list.size(); i++) {
        if(list.get(i) <= list.get(i-1)) {
            return false;
        }
    }
    return true;
}

530. 二叉搜索树的最小绝对差

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

中序遍历

BST中序遍历结果存储为有序数组,找出相邻两个元素的最小值。

ArrayList<Integer> list = new ArrayList<>();
public int getMinimumDifference(TreeNode root) {
    traversal(root);

    int min = list.get(1) - list.get(0);
    for(int i = 2; i < list.size(); i++) {
        if(list.get(i) - list.get(i-1) < min) {
            min = list.get(i) - list.get(i-1);
        }
    }

    return min;
}

// 中序遍历
private void traversal(TreeNode root) {
    if(root != null) {
        traversal(root.left);
        list.add(root.val);
        traversal(root.right);
    }
}

实际上不需要存到数组中,直接在遍历时判断即可。

public int getMinimumDifference(TreeNode root) {
    traversal(root);
    return min;
}

int min = Integer.MAX_VALUE;
TreeNode pre = null;  // 记录前一个节点
private void traversal(TreeNode root) {
    if(root != null) {
        traversal(root.left);
        if(pre != null) {
            if(root.val - pre.val < min) {
                min = root.val - pre.val;
            }
        }
        pre = root;
        traversal(root.right);
    }
}

501. 二叉搜索树中的众数

给定一个有相同值的二叉搜索树(BST),找出 BST 中的所有众数(出现频率最高的元素)。

假定 BST 有如下定义:

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

中序遍历

进行两次遍历,第一次记录最大频率值,第二次记录众数。

也可以在一次遍历完成,但是需要多次清空数组,即每当发现一个频率更高的数字,数组内保存的数字需要清除。

public int[] findMode(TreeNode root) {
    maxFreq = 0;
    currFreq = 0;
    pre = null;
    traversal(root, false);

    currFreq = 0;
    pre = null;
    list = new ArrayList<Integer>();
    traversal(root, true);

    int[] res = new int[list.size()];
    for(int i = 0; i < list.size(); i++) {
        res[i] = list.get(i);
    }
    return res;
}

int maxFreq;
int currFreq;
TreeNode pre;
ArrayList<Integer> list;

private void traversal(TreeNode root, boolean isFound) {
    if(root != null) {
        traversal(root.left, isFound);
        if(pre != null && pre.val == root.val) {
            currFreq++;
        }
        else {
            currFreq = 1;
        }
		
        // 是否已经找到了最大频率,用于区分第一次和第二次遍历
        if(isFound) {
            if(currFreq == maxFreq) {
                list.add(root.val);
            }
        }
        else {
            if(currFreq > maxFreq) {
                maxFreq = currFreq;
            }
        }

        pre = root;
        traversal(root.right, isFound);
    }
}

235. 二叉搜索树的最近公共祖先

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

根据路径判断

这个方法速度较慢。

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    ArrayList<TreeNode> pPath = findPath(root, p);
    ArrayList<TreeNode> qPath = findPath(root, q);
	
    // p和q的路径最后一个相同的节点即为最近公共祖先
    TreeNode ancestor = root;
    for(int i = 0; i < Math.min(pPath.size(), qPath.size()); i++) {
        if(pPath.get(i) != qPath.get(i)) {
            break;
        }
        ancestor = pPath.get(i);
    }
    return ancestor;
}

// 从root节点到target节点的路径
private ArrayList<TreeNode> findPath(TreeNode root, TreeNode target) {
    ArrayList<TreeNode> list = new ArrayList<>();
    if(root != null && target != null) {
        list.add(root);
        while(root != null && root.val != target.val) {
            if(root.val < target.val) {
                root = root.right;
            }
            else {
                root = root.left;
            }
            list.add(root);
        }
    }
    return list;
}

一次遍历

第一个分叉点(也是唯一一个交叉点)就是最近公共祖先。

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    while(root != null) {
        if(root.val < p.val && root.val < q.val) {  // p和q都在root的右边
            root = root.right;
        }
        else if(root.val > p.val && root.val > q.val) {  // p和q都在root的左边
            root = root.left;
        }
        else {  // 第一个分叉点就是最近公共祖先
            return root;
        }
    }
    return null;
}

236. 二叉树的最近公共祖先

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

根据路径判断

这个方法速度较慢。

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    isFound = false;
    LinkedList<TreeNode> pPath = findPath(root, p, new LinkedList<TreeNode>());
    isFound = false;
    LinkedList<TreeNode> qPath = findPath(root, q, new LinkedList<TreeNode>());
    
    // p和q的路径最后一个相同的节点即为最近公共祖先
    TreeNode ancestor = root;
    for(int i = 0; i < Math.min(pPath.size(), qPath.size()); i++) {
        if(pPath.get(i) != qPath.get(i)) {
            break;
        }
        ancestor = pPath.get(i);
    }
    return ancestor;
}

// 通过前序遍历和回溯,找到从root节点到target节点的路径
boolean isFound;
private LinkedList<TreeNode> findPath(TreeNode root, TreeNode target, LinkedList<TreeNode> path) {
    if(root != null && !isFound) {
        path.add(root);
        if(root == target) {
            isFound = true;
            return path;
        }
        findPath(root.left, target, path);
        findPath(root.right, target, path);
        if(!isFound){
            path.removeLast();
        }
    }
    return path;
}

后序遍历

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    if (root == null || root == p || root == q) {
        return root;
    }
    TreeNode left = lowestCommonAncestor(root.left, p, q);
    TreeNode right = lowestCommonAncestor(root.right, p, q);
    
    // 只有第一个分叉点才会左右都不为null
    if (left != null && right != null) {
        return root;
    }
    if (left == null) {
        return right;
    }
    return left;
}

另一种后序遍历,原理类似,即后序遍历第一次碰到的交叉点(也是唯一一个交叉点)为最近公共祖先,交叉点有两种情况:

  • 当前节点的左子树包含p或q,且右子树也包含p或q
  • 当前节点为p或q,且左子树或右子树包含p或q
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    traversal(root, p, q);
    return ancestor;
}

TreeNode ancestor;
// 节点root的子树是否包含p或q
private boolean traversal(TreeNode root, TreeNode p, TreeNode q) {
    if(root == null) return false;

    boolean left = traversal(root.left, p, q);
    boolean right = traversal(root.right,p, q);
	
    // 判断root是否是交叉点
    if(left && right || ((left || right) && (root == p || root == q))) {
        ancestor = root;
    }
    // 判断root是否包含p或q
    if(left || right || root == p || root == q) {
        return true;
    }
    return false;
}

701. 二叉搜索树中的插入操作

给定二叉搜索树(BST)的根节点和要插入树中的值,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同

public TreeNode insertIntoBST(TreeNode root, int val) {
    TreeNode node = new TreeNode(val);
    if(root == null) return node;
	
    // 比当前节点小就向左,大就向右,直到遇到空节点就可以进行插入操作
    TreeNode curr = root;
    while(true) {
        if(curr.val > val) {
            if(curr.left != null) {
                curr = curr.left;
            }
            else {
                curr.left = node;
                break;
            }
        }
        else if(curr.val < val) {
            if(curr.right != null) {
                curr = curr.right;
            }
            else {
                curr.right = node;
                break;
            }
        }
    }
    return root;
}

递归

public TreeNode insertIntoBST(TreeNode root, int val) {
    if(root == null) return new TreeNode(val);

    if(root.val > val) root.left = insertIntoBST(root.left, val);
    if(root.val < val) root.right = insertIntoBST(root.right, val);

    return root;
}

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

给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。

一般来说,删除节点可分为两个步骤:

  1. 首先找到需要删除的节点;
  2. 如果找到了,删除它。

image

public TreeNode deleteNode(TreeNode root, int key) {
    if(root == null) return null;
    if(root.val == key) {  // 删除当前节点,返回新节点
		return delRoot(root);
    }
    // 递归地向左右子树查找
    // 如果root的左子节或右节点不需要被删除,则保持原状
    // 如果需要被删除,则修改为删除后的新节点
    else if(root.val > key) {
        root.left = deleteNode(root.left, key);
    }
    else if(root.val < key) {
        root.right = deleteNode(root.right, key);
    }
    return root;
}

// 删除根节点root的过程
private TreeNode delRoot(TreeNode root) {
    // 如果root的左节点不为null, 则挑选左子树中的最右节点作为新的root
    if(root.left != null) {
        TreeNode curr = root.left;
        if(curr.right == null) {  // root的左节点就恰好为最右节点的情况
            curr.right = root.right;
        }
        else {  
            // 找到root的左子树中的最右节点
            TreeNode pre = curr;
            while(curr.right != null) {
                pre = curr;
                curr = curr.right;
            }
            // 将找到的最右节点作为新的root
            pre.right = curr.left;
            curr.left = root.left;
            curr.right = root.right;

        }
        return curr;
    }
    else {  // root左子树为空,那么直接将右节点作为新的root
        return root.right;
    }
}

669. 修剪二叉搜索树

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

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

// 后序遍历,自底向上修剪
public TreeNode trimBST(TreeNode root, int low, int high) {
  if(root == null) return null;
  root.left = trimBST(root.left, low, high);
  root.right = trimBST(root.right, low, high);
  return remove(root, low, high);
}

// 由于是二叉搜索树,如果当前节点需要修剪,则需要将其左子树或右子树一起修剪
private TreeNode remove(TreeNode root, int low, int high) {
  if(root == null) return null;
  if(root.val < low) {
    return root.right;
  }
  if(root.val > high) {
    return root.left;
  }
  return root;
}

108. 将有序数组转换为二叉搜索树

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

高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。

递归

public TreeNode sortedArrayToBST(int[] nums) {
    return buildTree(nums, 0, nums.length);
}

// 每次递归根据数组最中间的数构建新节点
private TreeNode buildTree(int[] nums, int left, int right) {
    if(left >= right) return null;
    int mid = (left + right) / 2;
    TreeNode root = new TreeNode(nums[mid]);
    root.left = buildTree(nums, left, mid);
    root.right = buildTree(nums, mid + 1, right);
    return root;
}

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

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

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

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

中序遍历

public TreeNode convertBST(TreeNode root) {
    traversal(root);
    sumList();
    update(root);
    return root;
}

// 将中序遍历的结果存储到数组list中
private ArrayList<Integer> list = new ArrayList<>();
private void traversal(TreeNode root) {
    if(root != null) {
        traversal(root.left);
        list.add(root.val);
        traversal(root.right);
    }
}

// 将list数组倒序累加
private void sumList() {
    for(int i = list.size()-2; i >= 0; i--) {
        list.set(i, list.get(i) + list.get(i+1));
    }
}

// 再次中序遍历,根据累加后的list数组更新树
private int index = 0;
private void update(TreeNode root) {
    if(root != null) {
        update(root.left);
        root.val = list.get(index);
        index++;
        update(root.right);
    }
}

反中序遍历

其实,只需要“右中左”的反中序遍历,记录上一个节点pre,每次将当前节点的值加上pre的值即可。

private TreeNode pre = null;  // 记录上一个节点的值
public TreeNode convertBST(TreeNode root) {
    if(root == null) return null;
    convertBST(root.right);
    if(pre != null) {
        root.val += pre.val;
    }
    pre = root;  // 更新pre的值
    convertBST(root.left);
    return root;
}
posted @ 2021-12-13 11:19  lv6laserlotus  阅读(45)  评论(0)    收藏  举报