LeetCode-树

6 篇文章 0 订阅
6 篇文章 0 订阅

一. 前言

​ 树也是一种常见的数据结构,在算法题里最常见的就是数组,链表,哈希表以及树。前三者平时很常用到,所以也不会觉得太难。树如果刚开始没有练习,会觉得很难,但在经过一定的练习和总结之后,会觉得树的题目也是比较容易解决的。这里目前只记录了比较常见且典型的树型题目,一些奇形怪状的树题暂时不考虑。

二. 关于递归

​ 递归是一种常见的解题方法,主要特征为反复调用自身。递归会让代码看起来很简洁,逻辑也很清晰,但有时候也会造成大量的重复计算。二叉树跟链表是很适合进行递归的数据结构,因为它们的数据就像一段一段的,递归可以使我们把思考的重点放在“当前层次的递归应该做什么”,而不必陷入整个复杂数据结构的复杂思考。

​ 递归的核心是反复调用自身,我们有3个点需要重点思考:

①既然是反复调用自身,那么什么时候结束?即我们需要寻找递归的终止条件

②应该返回什么?对于第一层递归,返回值就是最终结果。同时第一层需要调用第二层的递归结果,那么第二层需要返回给上一级(也就是第一层)什么样的结果?

③每一层递归应该做什么?也就是当前层次的递归,需要进行什么逻辑?完成什么任务?

​ 对于接下来的树题,我们都会优先思考递归解题模板,然后再寻找迭代式的解题方法。

三. 二叉树的深度

① 最大深度 LeetCode 104

描述: 给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。题目链接

分析:先以递归的思路来思考:

① 如果要用递归,什么时候结束?显然,当root为null的时候,就不必继续递归了,此时直接返回0。

② 应该返回什么?其实每一层的返回值都是一样的,因为递归是调用自身,所以不存在说第一层返回的是xxx概念,而其他层返回的是yyy概念。那么第一层返回什么呢,就是返回根节点root的深度。所以后续的每一层也是一样的,就是返回以当前结点为root的树的深度。

③每一层递归应该做什么?终止条件,返回值我们都已经想好了,这时候就是需要中间的逻辑。在当前层,我们需要获得root的深度,这时候我们要递归下去,就是把root.left和root.right成为下一层递归的根节点,这样我们就求得root的左子树深度,以及右子树的深度,然后它们二者较大的值加上root本身深度(也就是1),就是root的最大深度。所以 root的深度 = 1 + max(root.left的深度, root.right的深度)。

这就是递归的优雅之处,我们只需要考虑当前层的逻辑,对于当前层,root的深度只要考虑root.left的深度以及root.right的深度,至于root.left的结构是什么,是否为null,root.left的最大深度是root.left.left还是root.left.right?这些在下一层就会变成和当前层一样的逻辑,我们无须考虑。于是我们可以获得代码:

代码

class Solution {
    public int maxDepth(TreeNode root) {
        if (root == null)
            return 0;
        int leftDepth = maxDepth(root.left);
        int rightDepth = maxDepth(root.right);
        return 1 + Math.max(leftDepth, rightDepth);
    }
}

如果你更熟练,会发现leftDepth与rightDepth变量也是可以省略的,这时候就能写出更简洁的代码:

class Solution {
    public int maxDepth(TreeNode root) {
        return root == null ? 0 : 
        	1 + Math.max(maxDepth(root.left), maxDepth(root.right));
    }
}

一行解决!相信你已经体会到了递归的美妙之处。当然,其实这里还是会产生了很多重复运算,所以可以使用一个Map来存储数据,提升效率。不要看LeetCode上效率更慢,那是测试用例的问题,如果存在一个巨型二叉树,上面的代码需要很长的时间,而使用Map会提升不少效率。

使用 Map作备忘录的递归代码

class Solution {
    public int maxDepth(TreeNode root) {
        Map<TreeNode, Integer> map = new HashMap<>();
        int res = helper(root, map);
        return res;
    }

    public int helper(TreeNode root, Map<TreeNode, Integer> map) {
        if (root == null)
            return 0;
        if (map.containsKey(root))
            return map.get(root);
        int left = helper(root.left, map);
        int right = helper(root.right, map);
        int res = 1 + Math.max(left, right);
        map.put(root, res);
        return res;
    }
}

递归解法很简单,如果不允许用递归呢,如果一定要用迭代的方法,应该如何编码?这个我们等会再说。

② 最小深度 LeetCode 111

描述: RT。题目链接

分析:最大深度的递归结束是当结点为null的时候,而最小深度则是遇到第一个叶子结点就返回,同时要取min而不是max,于是很容易写出这种代码:

class Solution {
    public int minDepth(TreeNode root) {
        if (root == null)
            return 0;
        if (root.left == null && root.right == null)
            return 1;
        return 1 + Math.min(minDepth(root.left), minDepth(root.right));
    }
}

然后就错了,因为max我们可以确保肯定会选最大的子树长度,而min可能会选择了空子树,使得结果出错,比如测试用例[1, 2],这时候右子树深度为0,答案就变成了1,而不是2,所以只有当root.left,root.right不为null的时候才放进去递归。当然,最开始的判断root是否为null还是不能省略的,因为可能输入的参数就刚好是root。

正确的代码

class Solution {
    public int minDepth(TreeNode root) {
        if (root == null)
            return 0;
        if (root.left != null && root.right == null)
            return 1 + minDepth(root.left);
        if (root.right != null && root.left == null)
            return 1 + minDepth(root.right);
        return 1 + Math.min(minDepth(root.left), minDepth(root.right));
        // 如果都为null, 也不会影响结果
    }
}

四. 树的遍历

描述:树的遍历是很常见的题,包括前序遍历,中序遍历,后序遍历,层序遍历,垂序遍历(前三种比较常见)。然后还存在根据前序与中序还原树,后序与中序还原树等等的题目。所以我们先从树的遍历开始总结(上一题主要解释一下递归的过程)。值得注意的是,有一些题目因为递归解法太过简单,所以有时候题目会要求“不要用递归,而是用迭代”,一下子就使得题目难度增加不少。所以这里会尽量通过两种方法来解决。

① 二叉树的前序遍历 LeetCode 144

描述:RT。题目链接

递归解法

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        preOrder(root, list);
        return list;
    }

    public void preOrder(TreeNode root, List<Integer> list) {
        if (root == null)		// 递归结束
            return;
        list.add(root.val);		// 当前层的操作
        preOrder(root.left, list);		// 递归
        preOrder(root.right, list);		// 递归
    }
}

如果要把递归解法转换成迭代解法,一般都可以通过栈来实现,因为递归实质也是使用栈,只是递归压栈的并不是数值对象,而是整个方法。所以才会有“递归栈"这种说法,在画出递归的过程时,实际上就是画递归栈。所以我们这里通过栈来放置树的结点,优先push右节点,因为后进先出,前序遍历的顺序是:根节点->左节点->右节点,所以先push右节点,再push左节点。

迭代解法

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        LinkedList<TreeNode> stack = new LinkedList<>();
        if (root == null)
            return res;
        stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode current = stack.pop();
            res.add(current.val);
            if (current.right != null)
                stack.push(current.right);	//先push right node
            if (current.left != null)
                stack.push(current.left);
        }
        return res;
    }
}

核心是先push right,然后处理所有的left结点。如果要与中序后序统一模板,可以改成下面的代码:

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        LinkedList<TreeNode> stack = new LinkedList<>();
        if (root == null)
            return res;
        TreeNode current = root;
        while (current != null || !stack.isEmpty()) {
            if (current != null) {
                res.add(current.val);	// root
                stack.push(current);
                current = current.left;	// left
            }
            else {
                current = stack.pop();	// right
                current = current.right;
            }
        }
        return res;
    }
}

也可以作出改进,使得只需要push right结点。

class Solution {
    // 只push right结点的方法
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        LinkedList<TreeNode> stack = new LinkedList<>();
        if (root == null)
            return res;
        TreeNode current = root;
        while (true) 
            if (current != null) {
                res.add(current.val);
                if (current.right != null)
                    stack.push(current.right);
                current = current.left;
            }
            else if (stack.isEmpty())
                return res;
            else
                current = stack.pop();
    }
}
② 二叉树的中序遍历 LeetCode 94

描述: RT。题目链接

递归解法

public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        inOrder(list, root);
        return list;
    }
    
public void inOrder(List<Integer> list, TreeNode root) {
    if (root == null)
        return;
    inOrder(list, root.left);
    list.add(root.val);
    inOrder(list, root.right);
}

迭代同样要用到栈,只是要先处理所有left结点,然后在后续的出栈中,先把当前的栈顶值加入到结果集中(left和root),然后再遍历right结点,以达到:left -> root -> right的效果。

迭代解法

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        LinkedList<TreeNode> stack = new LinkedList<>();
        if (root == null)
            return res;
        TreeNode current = root;
        while (current != null || !stack.isEmpty()) 
            if (current != null) {
                stack.push(current);	// 把所有left压栈
                current = current.left;
            }
            else {
                current = stack.pop();	// 出栈顺序,栈顶顺序为:left, root
                res.add(current.val);	// 添加到结果集
                current = current.right;	// 把right压栈
            }
        return res;
    }
}
③ 二叉树的后序遍历 LeetCode 145

描述: RT。题目链接

递归解法

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        postOrder(list, root);
        return list;
    }
    
    public void postOrder(List<Integer> list, TreeNode root) {
        if (root == null)
            return;
        postOrder(list, root.left);
        postOrder(list, root.right);
        list.add(root.val);
    }
}

后序遍历是在LeetCode中,三个遍历中唯一一个难度为hard的题目(当然,前提是不考虑递归)。这是因为前序遍历为:root -> left -> right,中序遍历为: left -> root -> right,而在压栈过程中,实际上我们第一个压栈的元素永远是root,也就是第一个根节点。而后续的操作中,我们可以先处理root,然后按照right,left的顺序压栈,以达到前序遍历的目的。又或者是先处理left,然后出栈的时候处理完left再处理root,最后处理right,达到中序遍历的效果。但是后序遍历有一个很特别的点,它的顺序是: left -> right -> root,也就是说我们要最后处理root,这与前面两种都不相同。然后发现大部分的解法都比较tricky,先进行前序遍历,然后再更换顺序,把它变成后序遍历。前序遍历是先push right结点,这里更改为先push left结点,这样结果就是:root -> right -> left,然后再完全倒序,就变成了:left -> right -> root

tricky的迭代解法

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        LinkedList<TreeNode> stack = new LinkedList<>();
        if (root == null)
            return res;
        stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode current = stack.pop();
            res.add(0, current.val);	// addFirst, 使得最后的结果就是完全倒序
            if (current.left != null)
                stack.push(current.left);  // 先push left
            if (current.right != null)
                stack.push(current.right);
        }
        return res;
    }
}

这显然不是很好,其实后序的关键是,先遍历了left跟right子结点,然后才遍历当前结点,所以设置一个HashSet,记录已经访问过的结点。然后在获取栈顶元素的时候(不要直接出栈),先去确认子结点是否已经访问,如果已经访问了,这时候再把栈顶元素出栈,添加到结果集种。如果存在不为null的子结点却不在HashSet中,那么此时应该把子结点push到栈中,继续下一轮的循环。(同样地,先push right,再push left)

正确的迭代解法:

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        LinkedList<TreeNode> stack = new LinkedList<>();
        Set<TreeNode> set = new HashSet<>();    // visited
        if (root == null)
            return res;
        stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode current = stack.peek();
            boolean canVisited = true;
            // 先push right, 使得出栈优先是left, 达到 left -> right的效果
            if (current.right != null && !set.contains(current.right)) {
                canVisited = false;
                stack.push(current.right);
            }
            if (current.left != null && !set.contains(current.left)) {
                canVisited = false;
                stack.push(current.left);
            }
            // 只有left跟right都访问了,此时才访问root
            if (canVisited) {
                res.add(current.val);
                set.add(current);
                stack.pop();    // 访问了才出栈
            }
        }
        return res;
    }
}
④ 二叉树的层序遍历 LeetCode 102

描述:给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。 题目链接

递归解法

class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> res = new ArrayList<>();
        helper(root, res, 0);
        return res;
    }

    public void helper(TreeNode root, List<List<Integer>> res, int level) {
        if (root == null)
            return;
        if (res.size() == level) 
            res.add(new ArrayList<>());
        res.get(level).add(root.val);
        helper(root.left, res, level + 1);
        helper(root.right, res, level + 1);
    }
}

对于迭代,同样也会用到栈或者队列来存储结点数据。因为是当前层,所以我们可以逐层遍历,把每一层的结点全部添加到Queue当中,然后记录size。如果size为0,说明当前层已经为空,循环结束。如果不为0,这时候我们要把Queue里所有的结点都出列,同时添加到List中,在出列的过程中,我们会添加下一层的结点(当前结点的左节点或者右节点)。因为我们要确保只把当前层的结点全部出列,而下一层的结点会留在下一次循环(另起一个List),所以要保证后面来的数据不会影响到前面数据的顺序,最恰当的数据结构是队列。

class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> res = new ArrayList<>();
        if (root == null)
            return res;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(root);
        while (!queue.isEmpty()) {  // Queue为空,说明下一层没有任何结点
            List<Integer> cur = new ArrayList<>();
            int size = queue.size();    // 把Queue里的当前数据全部出列
            while (size > 0) {
                TreeNode current = queue.poll();
                cur.add(current.val);
                if (current.left != null)
                    queue.add(current.left);
                if (current.right != null)
                    queue.add(current.right);
                size--;
            }
            res.add(cur);   // 每一次循环就是遍历了一层
        }
        return res;
    }
}
⑤ 二叉树的垂序遍历 LeetCode 987

描述:给定二叉树,按垂序遍历返回其结点值。

对位于 (X, Y) 的每个结点而言,其左右子结点分别位于 (X-1, Y-1) 和 (X+1, Y-1)。

把一条垂线从 X = -infinity 移动到 X = +infinity ,每当该垂线与结点接触时,我们按从上到下的顺序报告结点的值( Y 坐标递减)。

如果两个结点位置相同,则首先报告的结点值较小。

按 X 坐标顺序返回非空报告的列表。每个报告都有一个结点值列表。

题目链接

分析:本题表述不够清楚。对于一个结点,先考虑它的x坐标(垂序),然后再考虑它的y坐标(同一垂线,从上往下输出),最后如果坐标相同(x,y都相同),再根据val的大小来输出。(这种遍历方式并不常见,也不实用,记住就好,前面4种的应用都很广泛)

class Solution {
        public List<List<Integer>> verticalTraversal(TreeNode root) {
        List<List<Integer>> res = new ArrayList<>();
        if (root == null)
            return res;
        Map<TreeNode, int[]> positions = new HashMap<>();
        LinkedList<TreeNode> queue = new LinkedList<>();
        queue.add(root);
        positions.put(root, new int[] {0, 0});
        int min = 0;    // 记录最左和最右
        int max = 0;
        while (!queue.isEmpty()) {      // 层序遍历, 记录每个端点的坐标
            int size = queue.size();
            while (size > 0) {
                TreeNode current = queue.poll();
                int[] pos = positions.get(current);
                if (current.left != null) {
                    queue.add(current.left);
                    positions.put(current.left, new int[] {pos[0] - 1, pos[1] + 1});
                    min = min <= pos[0] - 1 ? min : pos[0] - 1;
                }
                if (current.right != null) {
                    queue.add(current.right);
                    positions.put(current.right, new int[] {pos[0] + 1, pos[1] + 1});
                    max = max >= pos[0] + 1 ? max : pos[0] + 1;
                }
                size--;
            }
        }
        int length = max - min + 1;
        for (int i = 0; i < length; i++)    
            res.add(new ArrayList<>());

        List<int[]> list = new ArrayList<>();   // 改为使用三维数组存储{x, y, val}
        for (Map.Entry<TreeNode, int[]> entry: positions.entrySet()) {
            int[] value = entry.getValue();
            int[] tmp = new int[] {value[0], value[1], entry.getKey().val};
            list.add(tmp);
        }
        Collections.sort(list, new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                int x = o1[0] - o2[0];
                if (x != 0)
                    return x;
                int y = o1[1] - o2[1];
                if (y != 0)
                    return y;
                return o1[2] - o2[2];   // x, y, val
            }
        });
        for (int[] ints: list) {
            List<Integer> currentList = res.get(ints[0] - min);
            currentList.add(ints[2]);
            res.set(ints[0] - min, currentList);
        }
        return res;
    }
}

五. 根据两种遍历来还原树的结构

①前序 + 中序 LeetCode 105

主要思路:先从前序遍历中得知根结点,然后把根节点代入到中序遍历,把树划分成左子树和右子树,对子树进行递归,直到每一个子树都只有1个结点。直接看图操作:

之后继续对子树进行递归,左子树[9]已经无须继续操作,所以只需考虑右子树了。对右子树处理完毕之后,递归结束,也就得到了最后的结果。

class Solution {
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        if (preorder.length == 0)
            return null;
        int rootVal = preorder[0];
        int i;
        for (i = 0; i < inorder.length; i++)
            if (inorder[i] == rootVal)
                break;
        TreeNode root = new TreeNode(rootVal);
        int[] leftInorder = Arrays.copyOfRange(inorder, 0, i);
        int[] rightInorder = Arrays.copyOfRange(inorder, i + 1, inorder.length);
        int[] leftPreorder = Arrays.copyOfRange(preorder, 1, leftInorder.length + 1);
        int[] rightPreorder = Arrays.copyOfRange(preorder, 1 + leftPreorder.length, preorder.length);
        root.left = buildTree(leftPreorder, leftInorder);
        root.right = buildTree(rightPreorder, rightInorder);
        return root;
    }
}

这里的代码是比较偷懒的复制数组,实际上这会导致一定的时间开销。更好办法是,新建一个helper方法,传参增加起始的index。但主要的思路都是一样的。同时有一个细节,不要忘了去掉根节点,它在下一层循环里并不需要用到(我们只需要处理子树)。

② 后序 + 中序 LeetCode 106

主要思路:思路与上面一致,只是寻找根节点的方法改为了通过后序遍历。后序遍历的最后一个结点为根节点。后续的操作都一样,直接看图操作:

直接也是同样地递归,直到结束,就是最后的结果:

class Solution {
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        if (inorder.length == 0)
            return null;
        TreeNode root = new TreeNode(postorder[postorder.length - 1]);
        int index = 0;
        for (index = 0; index < inorder.length; index++)
            if (inorder[index] == postorder[postorder.length - 1])
                break;
        int[] inorder1 = new int[index];
        int[] inorder2 = new int[inorder.length - index - 1];
        System.arraycopy(inorder, 0, inorder1, 0, index);
        System.arraycopy(inorder, index + 1, inorder2, 0, inorder.length - index - 1);
        int[] postorder1 = new int[index];
        int[] postorder2 = new int[inorder.length - index - 1];
        System.arraycopy(postorder, 0, postorder1, 0, index);
        System.arraycopy(postorder, index, postorder2, 0, inorder.length - index - 1);
        root.left = buildTree(inorder1, postorder1);
        root.right = buildTree(inorder2, postorder2);
        return root;
    }
}

代码同样地,也直接复制数组了,优化方法同上,不再赘述。

六. 左叶子之和

描述: 计算给定二叉树的所有左叶子之和。LeetCode 404. 题目链接

分析:求和等问题也是直接递归就能解决,关键是明白什么是左叶子:必须是某一个结点的左子结点,同时它还是一个叶子,所以递归的时候自然而然是有3个判定条件的。

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

七. 路径和

① 路径总和 LeetCode 112.

描述:给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。

分析:一开始看错题目了,以为任意路径都可以,然后才发现一定要以“叶子结点"结束,所以要判断当前是否为叶子。用递归的解法也很显然,如果不为叶子,减去root的值,然后递归。

递归解法

class Solution {
    public 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);
    }
}

这道题的迭代解法,需要开辟另一个栈来存储当前和的结果(不能直接用一个普通变量,因为存在不同的路径,比如出栈左子节点和右子节点时,要分别考虑)。

class Solution {
    public boolean hasPathSum(TreeNode root, int sum) {
        if (root == null)
            return false;
        LinkedList<TreeNode> node_stack = new LinkedList<>();
        LinkedList<Integer> sum_stack = new LinkedList<>();
        node_stack.push(root);
        sum_stack.push(sum - root.val); // 每次node_stack pop,sum_stack减去该val
        TreeNode current;
        int currentSum;
        while (!node_stack.isEmpty()) {
            current = node_stack.pop();
            currentSum = sum_stack.pop();
            if (current.right == null && current.left == null && currentSum == 0)
                return true;    // 如果pop出来的是叶子,且sum变成0,说明是正确的路径
            if (current.right != null) {
                node_stack.push(current.right);
                sum_stack.push(currentSum - current.right.val);
            }
            if (current.left != null) {
                node_stack.push(current.left);
                sum_stack.push(currentSum - current.left.val);
            }
        }
        return false;
    }
}
② 路径总和 Ⅱ LeetCode 113.

描述:给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。

分析:像这种寻找所有路径的题目,很容易想到回溯算法,也就是DFS。在到终点的时候(叶子结点),判断是否符合结果(路径和是否为sum),如果是,添加到结果集。不管如何,后面都是返回上一层,也就是回溯,然后进行最关键的“状态reset”,也就是把上一个添加到list的值给去掉,也就是回溯的思路。

以本题基本测试用例 [5,4,8,11,null,13,4,7,2,null,null,5,1] 为例画出回溯过程:

可以先写出这题最基本的回溯模板:

class Solution {
    public List<List<Integer>> pathSum(TreeNode root, int sum) {
        List<List<Integer>> res = new ArrayList<>();
        List<Integer> list = new ArrayList<>();
        if (root == null)
            return res;
        helper(root, sum, 0, res, list);	// call the backtrack function
        return res;
    }

    public void helper(TreeNode root, int sum, int current,
                       List<List<Integer> res, List<Integer> list) {
        if (.....) {
            res.add(new ArrayList<>(list));
            return;	// backtrack
        }
        
        // while or for or if ...
    }
}

回溯的条件自然是到了叶子结点,此时判断是否为有效的路径,如果是,添加到结果集。回溯之后要把状态reset,所以后续都有list.remove(list.size() - 1)的操作。因为这里就是普通的前序遍历,所以也不需要while循环,直接if就可以解决了。

代码

class Solution {
    public List<List<Integer>> pathSum(TreeNode root, int sum) {
        List<List<Integer>> res = new ArrayList<>();
        List<Integer> list = new ArrayList<>();
        if (root == null)
            return res;
        list.add(root.val);
        helper(root, sum, root.val, res, list);	// root.val必须要有
        return res;
    }

    public void helper(TreeNode root, int sum, int current,
                       List<List<Integer>> res, List<Integer> list) {
        if (root.left == null && root.right == null && current == sum) {
            res.add(new ArrayList<>(list));
            return; // 回溯
        }
        if (root.left != null) {
            list.add(root.left.val);
            helper(root.left, sum, current + root.left.val, res, list);
            list.remove(list.size() - 1);
        }
        if (root.right != null) {
            list.add(root.right.val);
            helper(root.right, sum, current + root.right.val, res, list);
            list.remove(list.size() - 1);
        } 
    }
}
③ 路径总和 Ⅲ LeetCode 437.

描述:给定一个二叉树,它的每个结点都存放着一个整数值。

找出路径和等于给定数值的路径总数。

路径不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

二叉树不超过1000个节点,且节点数值范围是 [-1000000,1000000] 的整数。

分析:一开始觉得只是上一题的变种,做了一下才发现难多了,而这题竟然定义为Easy。我一开始是用背包问题的思维,当前root节点是否拿,分为拿和不拿,然后再分别递归到左子树和右子树。这时候的关键代码大概是:

return helper(root.left, sum, cur + root.val) + helper(root.left, sum, cur) + helper(root.right, sum, cur + root.val) + helper(root.right, sum, cur)

毁掉这段代码的倒也跟上一题的测试用例一样,“0”。当一个结点的值为0,或者sum为0,这时候这个代码就很多错误。可能会重复计算,也可能会少计算。后来还是要额外增加一个数据结构,也就是HashMap,用于记录到达当前元素的路径上,之前的所有元素的和(根节点到当前元素)。直觉上会觉得,该题路径不要求从根结点开始。但假设路径的起始结点为node1,它具有父节点node2,node2有父节点node3,结束结点为node0。

这时候应该是:node0 - node1 == sum

但我们记录的是:
f (node0) = node0 + node2 + node3
f (node1) = node1 + node2 + node3

所以: f(node0) - f(node1) = = node0 - node1 == sum

即结果是一样的。这个概念正式地称为 前缀和。值得注意的是,因为DFS实际上也是回溯的过程,那么回溯的时候也是要进行状态reset的,否则后面的结点在回溯的时候会影响前面的状态。

代码

class Solution {
    public int pathSum(TreeNode root, int sum) {
        if (root == null)
            return 0;
        Map<Integer, Integer> map = new HashMap<>();
        map.put(0, 1);
        return helper(root, sum, 0, map);        
    }

    public int helper(TreeNode root, int sum, int prev, Map<Integer, Integer> map) {
        if (root == null)
            return 0;
        int res = 0;
        prev += root.val;
        res += map.getOrDefault(prev - sum, 0); 
        // 当前和为prev,寻找前面和为prev - sum的,差值就是sum(从后往前推)
        
        map.put(prev, map.getOrDefault(prev, 0) + 1);   // 当前节点的前缀和

        res += helper(root.left, sum, prev, map);
        res += helper(root.right, sum, prev, map);      // 下一层

        map.put(prev, map.get(prev) - 1);       // 回溯, 状态reset
        return res;
    }
}

八. 二叉搜索树BST

二叉搜索树又称BST,其特点为左子树的元素都小于根结点,右子树的元素都大于根节点。这是不考虑重复元素的情况,如果考虑,可以规定相同的元素往左子树放,或者右子树放,但此时复杂性还是增加不少,对于查询的时候可能需要分别递归查找两个子树,所以一般考虑无重复元素的BST即可。

① 修剪二叉搜索树 LeetCode 669.

描述:给定一个二叉搜索树,同时给定最小边界L 和最大边界 R。通过修剪二叉搜索树,使得所有节点的值在[L, R]中 (R>=L) 。你可能需要改变树的根节点,所以结果应当返回修剪好的二叉搜索树的新的根节点。

分析:做树的题目一定要记住,先考虑递归,再考虑其他的。因为树的结构很复杂,如果你直接把问题整体地考虑,很难有思路。比如这一题,你可能需要改变根结点,那么修剪之后剩下哪些结点?哪个结点应该作为根节点?如果先得出剩余的结点,然后再手动构造BST,这个显然不是一个好办法。所以就要把问题细化,也就是回归递归的三个步骤。(为什么突然又要拿出“三大步骤”来做题?因为上一题把我卡了好久= =)

  1. 递归什么时候结束?
    显然,root为null就结束,这时候就直接返回null
  2. 应该返回什么?
    同样地,每一层的返回值都是相同的,既然第一层返回的是根结点,所以每一层返回的都是根节点
  3. 每一层递归应该做什么?
    既然要考虑的是根节点,所以自然就是先判断根节点与L,R的关系比较。如果root的值小于L,那么root就要去掉,此时最恰当的是返回比root大一点的结点,也就是root.right。大于R同理。如果root.right仍然不符合怎么办?同样地,这是下一层递归要考虑的,我们在当前层不需要考虑。那么在处理完root之后是否就结束了呢?并不,此时只是说明这个root不需要改变了,这时候还要递归地考虑root的左子树和右子树的值,它们有可能不属于[L, R]范围。处理方法也是同样地,一层一层循序渐进,所以考虑的是root.left, root.right

代码

class Solution {
    public TreeNode trimBST(TreeNode root, int L, int R) {
        if (root == null)
            return null;
        // 返回右子树, 因为right >= root,如果仍然不符合,继续递归
        if (root.val < L)
            return trimBST(root.right, L, R);   
        if (root.val > R)
            return trimBST(root.left, L, R);    // 同上
        // 处理子结点   (root满足了,递归去除子树不符合的值)
        root.left = trimBST(root.left, L, R);
        root.right = trimBST(root.right, L, R);
        return root;
    }
}
② 二叉搜索树中第K小的元素 LeetCode 230.

描述:给定一个二叉搜索树,编写一个函数 kthSmallest 来查找其中第 k 个最小的元素。

说明:
你可以假设 k 总是有效的,1 ≤ k ≤ 二叉搜索树元素个数。

分析:对BST进行中序遍历,将得到一个有序的线性表,因而再获取第K小的元素就很简单。

class Solution {
    public int kthSmallest(TreeNode root, int k) {
        List<Integer> list = new ArrayList<>();
        inOrder(root, list, k);
        return list.get(k - 1);
    }

    public void inOrder(TreeNode root, List<Integer> list, int k) {
        if (root == null || list.size() == k)
            return;
        inOrder(root.left, list, k);
        list.add(root.val);
        inOrder(root.right, list, k);
    }
}

看了一下排名最高的解法,其实也是中序遍历,只是用了全局变量提升了效率而已:

class Solution {
    int res = 0;
    int k = 0;
    public int kthSmallest(TreeNode root, int k) {
        this.k = k;
        dfs(root);
        return res;
    }

    public void dfs(TreeNode root) {
        if (root == null)
            return;
        dfs(root.left);
        k--;
        if(k == 0)
            res = root.val;
        dfs(root.right);
    }
}

进阶: 如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化 kthSmallest 函数?

这时候可以维护一个大小为k的堆。如果要获取第k小,那么就维护最大堆,此时堆顶元素就是第k小。一次维护的时间复杂度为O(logN),效率很高。

③ 把二叉搜索树转换成累加树 LeetCode 538.

描述:给定一个二叉搜索树(Binary Search Tree),把它转换成为累加树(Greater Tree),使得每个节点的值是原来的节点值加上所有大于它的节点值之和。

分析:如果愿意,仍然可以直接中序遍历,然后再对List进行累加。但其实这道题需要的是从右往左添加(最右的元素不需要操作,倒数第二右的元素只需要添加最右元素,以此类推)。所以可以把遍历顺序改成:right -> root -> left,使用一个全局变量记录右边的元素值。

代码

class Solution {
    int right = 0;
    public TreeNode convertBST(TreeNode root) {
        inOrder(root);
        return root;
    }
    public void inOrder(TreeNode root) {
        if (root == null)
            return;
        inOrder(root.right);
        root.val += right;	// 加上右边的元素
        right = root.val;	// right更新为现在的root.val
        inOrder(root.left);
    }
}

因为这个递归的方法只有一个参数,让人很难去尝试直接去掉,递归convertBST方法本身,于是可以化简代码:

class Solution {
    int right = 0;
    public TreeNode convertBST(TreeNode root) {
        if (root != null) {
            convertBST(root.right);
            root.val += right;
            right = root.val;
            convertBST(root.left);
        }
        return root;
    }
}
④ 二叉搜索树的最近公共祖先 LeetCode 235

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

分析:可以一直递归,直到找到最近的公共祖先,但是这就浪费了BST的条件,所以直接迭代就可以了。根节点root肯定是二者的公共祖先,迭代逐步找到最近的公共祖先。这时候root与p,q的大小只有四种可能:root介于二者之间,root比二者都大,root比二者都小,与二者之一相等。对于第一种情况,此时root是公共祖先,因而root.left跟root.right都不会是公共祖先了(root位于p,q中间,而root是从根节点开始迭代的)。对于第二种情况,p,q都位于root的左子树,所以直接:root = root.left即可找到更近的公共祖先,第三种情况同理。第四种情况,显然也是直接返回(root与其中之一相等,且是它们两个的公共祖先,那自然是最近的公共祖先了)

代码

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        while (root != p && root != q) {
            if ((p.val < root.val && q.val > root.val) ||
                (q.val < root.val && p.val > root.val))
                return root;    
            // 如果root是公共祖先, 那么root.left跟root.right都不会是公共祖先(root是p,q中间)
            else if (p.val < root.val)  //p, q < root
                root = root.left;
            else
                root = root.right;      // p, q > root
        }
        return root;
    }
}
⑤ 二叉树的最近公共祖先 LCA问题 LeetCode 236

描述:与上一题相同,只是这一题是普通的二叉树,而不是BST。题目链接

分析:可以创建一个判定是否为子结点的方法,然后对root进行递归。这样显然就进行了两次O(n ^ 2)的循环,所以效率非常低。并不推荐。这时候我们再次拿出递归三步骤来进行解题:

  1. 递归什么时候结束?
    当root为null的时候,或者等于p或q二者之一。(其实就是问边界情况,但递归就是有了这个边界才不会无限循环下去)
  2. 应该返回什么?
    既然是调用自身,所以同样地,返回以当前root为根节点的树,对于结点p,q的最近公共祖先
  3. 每一层递归应该做什么?
    判断root是否为LCA,如果是,直接返回,如果不是,继续递归左子树和右子树。其实每一次递归都只有4种可能:null(已经到达边界),p/q(如果root等于二者之一,同样也可以直接返回),root.left,root.right.显然,当root == null || root == p || root == q,此时直接返回root,因为root就是LCA(跟BST一样)。如果不是,自然就是分别递归root.left,root.right,根据它们的结果来判定:

​ ① root.left,root.right的返回值都不为null。说明当前的root就是LCA (就像BST的if情况)

​ ② root.left的返回值为null,root.right返回值不为null。显然LCA在右子树,直接返回root.right的返回值

​ ③root.left不为null,root.right为null。同上。

​ ④二者都为null。return null。实际上不会到达,所以情况3和4直接写成else也是可以的。

代码

class Solution {
    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);
        if (left != null && right != null)
            return root;	// left最后递归到p, right递归到q,即分别在两侧, 自然当前root就是LCA
        else if (left != null)
            return left;
        else 
            return right;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值