刷题笔记之二叉树

前言

本博客是对二叉树做相关练习时所做笔记,主要内容来源二叉树
文中首先介绍了树的常见遍历方法,包括递归与迭代,然后又介绍了递归在树中的重要作用,以及相关题目的练习。

  • 是一种用来模拟具有树状结构性质的数据集合;树里的每一个结点有一个和一个包含所有子结点的列表

    从图的观点看,树也可视为一个拥有N个结点N-1条边的有向无环图。

  • 二叉树是一种典型的树状结构,每个结点最多有两个子树,即左子树右子树

树的遍历

树的遍历可分为前序遍历中序遍历后序遍历层序遍历

  • 前序遍历首先访问根节点,然后前序遍历左子树,最后前序遍历右子树,即根左右中序遍历顺序为左根右后序遍历左右根
  • 层序遍历:逐层地,从左到右访问所有结点。

144.二叉树的前序遍历

  • 题目描述:给你二叉树的根节点 root ,返回它结点值的 前序 遍历。递归算法很简单,你可以通过迭代算法完成吗?

  • 分析

    递归很简单,按照根左右的顺序即可,终止条件为root==null

    而迭代法,我们要使用栈来模拟整个过程。前面在DFS中使用栈完成了前序遍历,方法是访问完当前结点后,将其相邻结点全部入栈,其并没有还原递归的过程,这里换一种迭代方法。

    我们观察到,在递归中,首先访问根结点,然后就递归遍历左子树,这个过程会先把最左边结点全部入栈,为null时,就返回上一级递归,也就是出栈,然后递归遍历右子树。我们可使用栈来模拟这个过程。

  • 代码

    //递归
    class Solution {
        public List<Integer> preorderTraversal(TreeNode root) {
            List<Integer> res = new ArrayList<>();
            preorder(root,res);
            return res;
        }
    
        public void preorder(TreeNode root, List<Integer> res) {
            if(root != null) {
                //首先访问根结点
                res.add(root.val);
                //遍历左子树
                preorder(root.left,res);
                //遍历右子树
                preorder(root.right,res);
            }
        } 
    }
    //迭代
    class Solution {
        public List<Integer> preorderTraversal(TreeNode root) {
            List<Integer> res = new ArrayList<>();
            if(root == null) return res;
            Stack<TreeNode> stack = new Stack<>();
    
            while(root!=null || !stack.empty()) {
                //将左结点全部入栈,相当于preorder(root.left,res)执行完
                while(root != null) {
                    //首先访问根结点
                    res.add(root.val);
                    stack.push(root);
                    root = root.left;
                }
                root = stack.pop();
                //遍历右子树
                root = root.right;
            }
    
            return res;
        }
    }
    

94.二叉树的中序遍历

  • 题目描述:给定一个二叉树的根节点 root ,返回它的 中序 遍历。

  • 分析

    递归很好理解,按照左根右的顺序即可。

    迭代法我们依旧使用栈来进行模拟,根据递归法,对于当前结点cur,首先遍历左子树,这个过程会一直向左直至最左边的结点left,然后访问结点left,再遍历结点left的右子树。

    因此,我们可以从根结点开始,依次将左结点入栈,直至为null,然后出栈,即为最左边的结点,访问该结点(加入结果集),然后再遍历其右子树。

  • 代码

    //递归
    class Solution {
        public List<Integer> inorderTraversal(TreeNode root) {
            List<Integer> res = new ArrayList<>();
            inorder(root,res);
            return res;
        }
    
        public void inorder(TreeNode root, List<Integer> res) {
            if(root != null) {
                //遍历左子树
                inorder(root.left,res);
                //访问根结点
                res.add(root.val);
                //遍历右子树
                inorder(root.right,res);
            }
        }
    }
    
    //迭代,可看到与前序遍历的迭代法的区别仅仅在于访问结点的位置不同
    class Solution {
        public List<Integer> preorderTraversal(TreeNode root) {
            List<Integer> res = new ArrayList<>();
            if(root == null) return res;
            
            Stack<TreeNode> stack = new Stack<>();
            
            while(root!=null || !stack.empty()) {
                //将左结点全部入栈,相当于preorder(root.left,res)执行完
                while(root != null) {
                    stack.push(root);
                    root = root.left;
                }
                root = stack.pop();
                //访问结点
                res.add(root.val);
                //遍历右子树
                root = root.right;
            }
            
            return res;
        }
    }
    

145.二叉树的后序遍历

  • 题目描述:返回一个二叉树的后序遍历。

  • 分析

    对于迭代法,一个讨巧的方法就是在前序遍历的基础上去写。

    前序遍历是根左右,我们也可以获取根右左的顺序,反转后就是左右根,即后序遍历的顺序。

    而如果要直接通过迭代进行后序遍历,依旧是首先到达最左边结点,我们观察到,在到达最左边的结点之后,我们不能直接访问该结点,还要判断该结点是否有右子树或者右子树是否已经被访问过,如果没有右子树或者右子树已被访问过再去访问该结点,有右子树并且未被访问过就要转到其右子树中,为了遍历完右子树后能遍历根结点,我们在转到右子树之前需要再次将当前结点入栈。

  • 代码

    //递归
    class Solution {
        public List<Integer> postorderTraversal(TreeNode root) {
            List<Integer> res = new ArrayList<>();
            postorder(root,res);
            return res;
        }
    
        public void postorder(TreeNode root, List<Integer> res) {
            if(root != null) {
                //遍历左子树
                postorder(root.left,res);
                //遍历右子树
                postorder(root.right,res);
                //访问根结点
                res.add(root.val);
            }
        }
    }
    
    //迭代1,通过前序遍历后反转
    class Solution {
        public List<Integer> postorderTraversal(TreeNode root) {
            List<Integer> res = new ArrayList<>();
            if(root == null) return res;
            
            Stack<TreeNode> s = new Stack<>();
            Stack<Integer> s2 = new Stack<>();
            s.push(root);
            
            while(!s.empty()) {
                TreeNode cur = s.pop();
                s2.add(cur.val);
                //相邻结点入栈,先左后右
                if(cur.left != null) {
                    s.push(cur.left);
                }
                if(cur.right != null) {
                    s.push(cur.right);
                }
            }
            while(!s2.empty()) {
                res.add(s2.pop());
            }
            return res;
        }
    }
    
    //迭代2,直接后序遍历
    class Solution {
        public List<Integer> postorderTraversal(TreeNode root) {
            List<Integer> res = new ArrayList<Integer>();
            if (root == null) {
                return res;
            }
    
            Stack<TreeNode> stack = new Stack<>();
            Set<TreeNode> visited = new HashSet<>();
    
            while (root != null || !stack.empty()) {
                while (root != null) {
                    stack.push(root);
                    root = root.left;
                }
                root = stack.pop();
                //判断当前结点是否有右子树
                if (root.right == null || visited.contains(root.right)) {
                    //没有右子树或右子树已访问过就直接访问当前结点
                    res.add(root.val);
                    //标记当前结点已被访问
                    visited.add(root);
                    root = null;
                } else {
                    //右子树并且未被访问就再次将当前结点入栈,转向右子树,这样能做到遍历完右子树后再访问根结点
                    stack.push(root);
                    root = root.right;
                }
            }
            return res;
        }
    }
    

模板总结

对于树的遍历,递归没什么好说的,这里主要总结一下迭代方法。

我们注意到,无论是前中后遍历,都需要先到达最左边的结点

所不同的是前序遍历先访问根结点,因此在结点入栈的时候就要访问结点;中序遍历先访问左结点,因此在到达最左边结点后再去访问结点;而后序遍历则是最后访问根结点,因此在到达最左边结点后还要先判断其是否有右子树或者右子树已被访问过再去访问该结点,有右子树并且未被访问过就要转到其右子树中,为了遍历完右子树后能遍历根结点,我们在转到右子树之前需要再次将当前结点入栈。

统一模板

public List<Integer> traversal(TreeNode root) {
    List<Integer> res = new ArrayList<>();
    if(root == null) return res;
    Stack<TreeNode> stack = new Stack<>();

    while(root!=null || !stack.empty()) {
        //先到达当前结点最左边的结点
        while(root != null) {
            ...
            stack.push(root);
            root = root.left;
        }
        //获取最左边的结点
        root = stack.pop();
        ...
        
    }

    return res;
}

具体添加位置

public List<Integer> traversal(TreeNode root) {
    List<Integer> res = new ArrayList<>();
    if(root == null) return res;
    Stack<TreeNode> stack = new Stack<>();

    while(root!=null || !stack.empty()) {
        //先到达当前结点最左边的结点
        while(root != null) {
            /**
             * 前序遍历在此处访问结点:res.add(root);
             */
            stack.push(root);
            root = root.left;
        }
        //获取最左边的结点
        root = stack.pop();
        
        /**
         * 中序遍历在此处访问结点:res.add(root);
         */
        
        /**
         * 前中序遍历只需直接遍历右子树:root = root.right;
         */
        
        /**
        后序遍历需判断当前结点是否有右子树或右子树是否已经被访问过,可用set集合
        if (root.right == null || set.contains(root.right)) {
            //没有右子树或右子树已访问过就直接访问当前结点
            res.add(root.val);
            //标记当前结点已被访问
            set.add(root);
            root = null;
        } else {
            //右子树并且未被访问就再次将当前结点入栈,转向右子树,这样能做到遍历完右子树后再访问根结点
            stack.push(root);
            root = root.right;
        }
        **/
        
    }

    return res;
}

102.二叉树的层序遍历

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

  • 示例

    二叉树:[3,9,20,null,null,15,7],
      	3
       / \
      9  20
        /  \
       15   7
    遍历结果
    [
      [3],
      [9,20],
      [15,7]
    ]
    
  • 分析

    典型的BFS,并且这里无需考虑结点是否重复,比较简单。

  • 代码

    /**
     * Definition for a binary tree node.
     * public class TreeNode {
     *     int val;
     *     TreeNode left;
     *     TreeNode right;
     *     TreeNode() {}
     *     TreeNode(int val) { this.val = val; }
     *     TreeNode(int val, TreeNode left, TreeNode right) {
     *         this.val = val;
     *         this.left = left;
     *         this.right = right;
     *     }
     * }
     */
    class Solution {
        public List<List<Integer>> levelOrder(TreeNode root) {
            List<List<Integer>> res = new ArrayList<>();
            if(root == null) return res;
    
            Queue<TreeNode> queue = new LinkedList<>();
            queue.offer(root);
            while(!queue.isEmpty()) {
                //每层结点的个数
                int size = queue.size();
                //当前层的结点值
                List<Integer> curLayer = new ArrayList<>();
                for(int i=0; i<size; i++) {
                    TreeNode cur = queue.poll();
                    curLayer.add(cur.val);
                    //将其邻居入队
                    if(cur.left != null) queue.add(cur.left);
                    if(cur.right != null) queue.add(cur.right);
                }
                res.add(curLayer);
            }
            return res;
        }
    }
    

递归在树中的应用

由于二叉树的结构特点,许多问题常常可以用递归来解决。

递归的关键:1.确定终止条件;2.找到本层与其它层的联系

104.二叉树的最大深度

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

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

  • 示例

    给定二叉树 [3,9,20,null,null,15,7],
     	3
       / \
      9  20
        /  \
       15   7
    返回它的最大深度 3 。
    
  • 分析

    我们自底向上考虑,如果当前结点为空,就返回深度为0,否则当前结点的深度等于其左右结点的深度的较大值加上1。

  • 代码

    /**
     * Definition for a binary tree node.
     * public class TreeNode {
     *     int val;
     *     TreeNode left;
     *     TreeNode right;
     *     TreeNode() {}
     *     TreeNode(int val) { this.val = val; }
     *     TreeNode(int val, TreeNode left, TreeNode right) {
     *         this.val = val;
     *         this.left = left;
     *         this.right = right;
     *     }
     * }
     */
    class Solution {
        public int maxDepth(TreeNode root) {
            //终止条件
            if(root == null) return 0;
            //与下一层的关系
            int left = maxDepth(root.left)+1;
            int right = maxDepth(root.right)+1;
            return Math.max(left,right);
        }
    }
    

101.对称二叉树

  • 题目描述:给定一个二叉树,检查它是否是镜像对称的。

  • 示例

    二叉树 [1,2,2,3,4,4,3] 是对称的。
    	1
       / \
      2   2
     / \ / \
    3  4 4  3
    
  • 分析

    要判断是否是镜像对称,我们可以使用两个指针。初始时指向根结点,一个指针左移时,另外一个指针右移,反之同理。这样就能保证两个指针指向的结点始终是对称结点,然后判断这两个结点的值是否相同即可。

  • 代码

    class Solution {
        public boolean isSymmetric(TreeNode root) {
            if(root == null) {
                return true;
            }
            return check(root.left,root.right);
        }
    
        //判断两个指针指向的结点是否对称
        public boolean check(TreeNode p1, TreeNode p2) {
            //终止条件
            if(p1==null && p2==null) return true;
            if(p1==null || p2==null) return false;
            //本层递归与下一层的关系
            return p1.val==p2.val && check(p1.left,p2.right) && check(p1.right,p2.left);
        }
    }
    

112.路径总和

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

  • 分析

    递归法:我们只需判断为叶子结点时和是否为 targetSum 即可,如果不是叶子结点,就继续对其左右子树递归。

    迭代法:相对复杂,但可帮助我们再一次熟悉BFS。除了用一个队列存储将要遍历的结点外,我们还要增加一个队列来存储从根结点到该结点的结点值总和。

  • 代码

    递归

    class Solution {
        public boolean hasPathSum(TreeNode root, int targetSum) {
            return check(root,targetSum,0);
        }
    
        public boolean check(TreeNode root, int targetSum, int sum) {
            //终止条件
            if(root == null) return false;
            sum += root.val; 
            //如果为叶结点
            if(root.left==null && root.right==null) {
                return sum==targetSum;
            }
    		//本层与下一层的关系
            return check(root.left,targetSum,sum) || check(root.right,targetSum,sum);
        }
    }
    

    迭代

    class Solution {
        public boolean hasPathSum(TreeNode root, int targetSum) {
            if(root == null) return false;
            //存放要遍历的结点
            Queue<TreeNode> nodeQueue = new LinkedList<>();
            //存放对应根结点到对应结点的结点值之和
            Queue<Integer> sumQueue = new LinkedList<>();
            //根结点入队列
            nodeQueue.offer(root);
            sumQueue.offer(root.val);
    
            while(!nodeQueue.isEmpty()) {
                TreeNode cur = nodeQueue.poll();
                int curSum = sumQueue.poll();
                //如果当前结点是叶子结点,判断是否等于目标和
                if(cur.left==null && cur.right==null && curSum==targetSum) {
                    return true;
                }
                //将当前结点的相邻结点加入队列
                if(cur.left != null) {
                    nodeQueue.offer(cur.left);
                    //对于的结点值之和也同时加入队列
                    sumQueue.offer(curSum + cur.left.val);
                }
                if(cur.right != null) {
                    nodeQueue.offer(cur.right);
                    sumQueue.offer(curSum + cur.right.val);
                }
            }
    
            return false;
        }
    }
    

题目练习

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

  • 题目描述:根据一棵树的前序遍历与中序遍历构造二叉树。

    注意:你可以假设树中没有重复的元素。

  • 示例

    前序遍历 preorder = [3,9,20,15,7]
    中序遍历 inorder = [9,3,15,20,7]
    
    返回如下的二叉树:
    
        3
       / \
      9  20
        /  \
       15   7
    
  • 分析

    我们知道,前序遍历先遍历根结点,因此前序遍历的第一个元素一定是当前子树的根结点;中序遍历根结点在中间,即左边是左子树右边是右子树,我们知道根结点的位置后便可在中序遍历中划分左子树和右子树,进而也能在前序遍历中划分左子树和右子树。

    我们可以根据上述性质用递归来解决问题,问题的关键就是如何获取前序遍历和中序遍历的左右子树,进而做下一轮的递归

    对于当前子树,首先根据前序遍历获取根结点;

    然后在中序遍历中找到根结点的索引,可直接遍历来寻找,也可使用哈希表(首先用一个map存放中序遍历的值和下标,然后就可以直接根据根结点的值获取其对应的下标了);

    找到索引后,我们就知道了中序遍历中的左右子树位置;

    由于前序遍历是按照‘根左右’的顺序排列,故只要知道左子树的长度也能获取前序遍历中左右子树的位置;

    最后我们便能对左右子树做下一轮递归。

  • 代码

    /**
     * Definition for a binary tree node.
     * public class TreeNode {
     *     int val;
     *     TreeNode left;
     *     TreeNode right;
     *     TreeNode() {}
     *     TreeNode(int val) { this.val = val; }
     *     TreeNode(int val, TreeNode left, TreeNode right) {
     *         this.val = val;
     *         this.left = left;
     *         this.right = right;
     *     }
     * }
     */
    class Solution {
        Map<Integer,Integer> map = new HashMap<>();
        public TreeNode buildTree(int[] preorder, int[] inorder) {
            for(int i=0; i<inorder.length; i++) {
                map.put(inorder[i],i);
            }
            return build(preorder, 0, preorder.length-1, inorder, 0, inorder.length-1);
        }
    
        public TreeNode build(int[] preorder, int l1, int r1, int[] inorder, int l2, int r2) {
            //终止条件
            if(l1 > r1) {
                return null;
            }
            //由前序遍历获取根结点
            int rootValue = preorder[l1];
            TreeNode root = new TreeNode(rootValue);
            //在中序遍历中根据根结点的位置划分左右子树
            int mid = map.get(rootValue);
            //左子树中元素个数
            int len = mid-l2;
            //构建左子树
            root.left = build(preorder, l1+1, l1+len, inorder, l2, mid-1);
            //构建右子树
            root.right = build(preorder, l1+len+1, r1, inorder, mid+1, r2);
    
            return root;
        }
    }
    

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

  • 题目描述:根据一棵树的中序遍历与后序遍历构造二叉树。

  • 分析

    此题与105题基本一样,所不同的是后序遍历的根结点在最后。

  • 代码

    class Solution {
        Map<Integer,Integer> map = new HashMap<>();
        public TreeNode buildTree(int[] inorder, int[] postorder) {
            for(int i=0; i<inorder.length; i++) {
                map.put(inorder[i],i);
            }
            TreeNode root = build(inorder, 0, inorder.length-1, postorder, 0, postorder.length-1);
            return root;
        }
    
    
        public TreeNode build(int[] inorder, int l1, int r1, int[] postorder, int l2, int r2) {
            //终止条件
            if(l1 > r1) {
                return null;
            }
            //由后序遍历获取根结点
            int rootValue = postorder[r2];
            TreeNode root = new TreeNode(rootValue);
            //在中序遍历中根据根结点的位置划分左右子树
            int mid = map.get(rootValue);
            //左子树中元素个数
            int len = mid-l1;
            //构建左子树
            root.left = build(inorder, l1, mid-1, postorder, l2, l2+len-1);
            //构建右子树
            root.right = build(inorder, mid+1, r1, postorder, l2+len, r2-1);
    
            return root;
        }
    }
    

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

  • 题目描述:给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:

    struct Node {
      int val;
      Node *left;
      Node *right;
      Node *next;
    }
    

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

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

    进阶:

    • 你只能使用常量级额外空间。
    • 使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度。
  • 分析

    最容易想到的方法就是BFS,我们使用层序遍历,以每一层为单位进行连接,其时间复杂度和空间复杂度都为O(n)

    如果要将空间复杂度降到常数级,就不能再使用队列了,可以使用已建立的next指针。一棵树中,对于相邻两个结点,只存在两种类型的next指针:父结点相同和父结点不同

    我们假设父结点为parent,如果父结点相同,则填充方式为parent.left.next = parent.right;如果父结点不同,则填充方式为parent.right.next = parent.next.left。可看到,本层的填充都是建立在上一层的基础之上,因此也要一层一层往下填充。

  • 代码

    /*
    // Definition for a Node.
    class Node {
        public int val;
        public Node left;
        public Node right;
        public Node next;
    
        public Node() {}
        
        public Node(int _val) {
            val = _val;
        }
    
        public Node(int _val, Node _left, Node _right, Node _next) {
            val = _val;
            left = _left;
            right = _right;
            next = _next;
        }
    };
    */
    
    class Solution {
        //使用BFS层序遍历
        public Node connect(Node root) {
            if(root == null) return root;
            Queue<Node> queue = new LinkedList<>();
            queue.offer(root);
    
            while(!queue.isEmpty()) {
                //记录当前层的结点数
                int size = queue.size();
                for(int i=0; i<size; i++) {
                    Node cur = queue.poll();
                    if(i < size-1){
                        cur.next = queue.peek();
                    }
                    
                    //将当前结点的左右结点加入队列
                    if(cur.left != null) {
                        queue.offer(cur.left);
                    }
                    if(cur.right != null) {
                        queue.offer(cur.right);
                    }
                }
            }
    
            return root;
        }
        
        //常数级额外空间——递归版
        public Node connect1(Node root) {
            if(root == null || root.left == null) return root;
            root.left.next = root.right;
            if(root.next != null) {
                root.right.next = root.next.left;
            }
            connect(root.left);
            connect(root.right);
            return root;
        }
        
        //常数级额外空间——迭代版
        public Node connect2(Node root) {
            if(root == null) return root;
            //根据最左边的结点来遍历每一层
            Node leftMost = root; 
            while(leftMost.left != null) {
                //当前父结点
                Node parent = leftMost;
                //对其子结点进行连接
                while(parent != null) {
                    //类型一
                    parent.left.next = parent.right;
                    //类型二
                    if(parent.next != null) {
                        parent.right.next = parent.next.left;
                    }
                    //右移,继续遍历当前层的其它结点
                    parent = parent.next;
                }
                //进入下一层
                leftMost = leftMost.left;
            }
    
            return root;
        }
    }
    

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

  • 题目描述:将116题的完美二叉树改为普通二叉树,其余条件不变。

  • 分析

    如果不考虑空间复杂度,我们可以用队列使用层序遍历,代码与116题完全一致。

    如果是常数级空间复杂度,改为普通二叉树后,我们不知道二叉树的结构了,因此不能像116题那样仅讨论两种情况,我们还需考虑左右结点是否存在,然后在此基础上再去填充next结点。

  • 代码

    class Solution {
        public Node connect(Node root) {
            if(root==null) return root;
    
            //填充左结点的下一个结点
            if(root.left!=null) {
                if(root.right!=null){
                    //右结点不为空
                    root.left.next=root.right;
                } else{
                    //右结点为空
                    root.left.next=getNext(root.next);
                }
            }
            
            //填充右结点的下一个结点
            if(root.right!=null) {
                root.right.next=getNext(root.next);
            }
    
            //先遍历右子树,因为左子树结点填充建立在右子树之上
            connect(root.right);
            connect(root.left);
            return root;
        }
        
        //获取以当前结点为根结点下一层的第一个结点
        public Node getNext(Node root){
            if(root==null) return null;
            if(root.left!=null) return root.left;
            if(root.right!=null) return root.right;
            if(root.next!=null) return getNext(root.next);
            return null;
        }
    }
    

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

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

    /**
     * 二叉树的结构
     * Definition for a binary tree node.
     * public class TreeNode {
     *     int val;
     *     TreeNode left;
     *     TreeNode right;
     *     TreeNode(int x) { val = x; }
     * }
     */
    
  • 分析一:迭代

    最直观的解法的就是我们分别找到从根结点到结点p和结点q的路径,然后在这两条路径中找到离根结点最远的相同结点,即为最近公共祖先。

    那么如何获取从根结点到另外一个结点的路径呢?

    一种方法就是我们使用DFS来遍历二叉树,然后用一个哈希表(HashMap)来存放每个结点和其对应的父结点,这样我们就可以利用结点中的父结点信息不断往上跳。具体过程可以先用一个集合(Set)记录结点p到根结点的路径,然后再从q结点不断往上跳,找到第一个相同的结点即为最近公共祖先。

    class Solution {
        //存放当前结点对应的父结点
        Map<Integer, TreeNode> parent = new HashMap<>();
        //通过DFS来存放除了根结点之外的每一个结点对应的父结点
        public void dfs(TreeNode root) {
            if(root.left != null) {
                parent.put(root.left.val, root);
                dfs(root.left);
            }
            if(root.right != null) {
                parent.put(root.right.val, root);
                dfs(root.right);
            }
        }
    
        public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
            if(root == null) return root;
            dfs(root);
            //set存放根结点到p的路径
            Set<TreeNode> set = new HashSet<>();
            //从p向上寻找到根结点的路径
            while(p != null) {
                set.add(p);
                p = parent.get(p.val);
            }
            //从q向上寻找到根结点的路径,如果当前结点在set中已存在,则为最近公共祖先
            while(q != null) {
                if(set.contains(q)) {
                    return q;
                }
                q = parent.get(q.val);
            }
    
            return null;
        }
    }
    

    另一种方法就是使用后序遍历用栈来存放从根结点到当前结点的路径,我们做过树的后序遍历题目。在迭代方式的后序遍历中,栈中的元素顺序刚好为根结点到当前结点的路径,具体看代码。这样我们用两个栈,一个存放p的路径,一个存放q的路径。然后通过出栈操作,找到第一个相同的栈顶元素即为最近公共祖先。这种方法虽然略微复杂,但能回顾后序遍历的迭代实现。

    class Solution {
        public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
            Stack<TreeNode> pStack = new Stack<>();
            Stack<TreeNode> qStack = new Stack<>();
            postorder(pStack,root,p);
            postorder(qStack,root,q);
    
            while(pStack.size() > qStack.size()) {
                pStack.pop();
            }
            while(pStack.size() < qStack.size()) {
                qStack.pop();
            }
            while(pStack.size() == qStack.size()) {
                if(pStack.peek().val == qStack.peek().val) {
                    return pStack.peek();
                }
                pStack.pop();
                qStack.pop();
            }
    
            return null;
        }
    
    	//迭代法后序遍历来找到root到target的路径
        public void postorder(Stack<TreeNode> stack, TreeNode root, TreeNode target) {
            //存放已经遍历过的结点
            Set<TreeNode> visited = new HashSet<>();
            while (root != null || !stack.empty()) {
                //最左边结点全部入栈
                while (root != null) {
                    stack.push(root);
                    root = root.left;
                }
                //最左边的结点
                root = stack.pop();
                //判断当前结点是否有右子树
                if (root.right == null || visited.contains(root.right)) {
                    //没有右子树或右子树已访问过就直接访问当前结点
                    if(root.val == target.val) {
                        //为目标结点将当前结点再次入栈,因为路径中包括当前结点
                        stack.push(root);
                        return;
                    }
                    //标记当前结点已被访问
                    visited.add(root);
                    //由于当前结点及其子树已被访问,需要继续弹出栈顶元素,故需要指向null
                    root = null;
                } else {
                    //有右子树并且未被访问,再次将当前结点入栈,转向右子树,这样能做到遍历完右子树后再访问根结点
                    stack.push(root);
                    root = root.right;
                }
            }
        }
    }
    
  • 分析二:递归

    此题比较巧妙的方法就是递归。

    首先理解什么情况下当前结点root为结点p和结点q的最近公共祖先,有三种情况:

    1. 结点p和结点q分别位于结点root的左右子树中,则root一定为pq的最近公共祖先;
    2. root是结点p,结点q在结点p的子树上,则结点ppq的最近公共祖先;
    3. root是结点q,结点p在结点q的子树上,则结点qpq的最近公共祖先;

    我们在写递归时,首先明白递归函数的功能是什么,然后再去判断终止条件,每层递归之间的关系等。

    函数的功能应该能判断当前子树是否存在结点p和结点q。如果存在,就返回它们的最近公共祖先;如果只存在一个,就返回存在的那一个;如果都不存在,就返回null。

  • 代码

    class Solution {
        public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
            //终止条件,注意这里包含了最近公共祖先为p或q的情况
            if(root==null || root==p || root==q) {
                return root;
            }
            
            //递归:因为是递归,使用函数后可认为左右子树已经算出结果
            //左子树中是否存在p或q
            TreeNode left = lowestCommonAncestor(root.left, p, q);
            //右子树中是否存在p或q
            TreeNode right = lowestCommonAncestor(root.right, p, q);
    		//左子树中没有p或q,最终结果只能在右子树中寻找,即为right
            if(left == null) return right;
            //右子树中没有p或q,最终结果只能在左子树中寻找,即为left
            if(right == null) return left;
    		
            //左右子树都有,说明root一定为最近公共祖先
            return root;
        }
    }
    

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

  • 题目描述:给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。

  • 分析一

    此题与236题的唯一区别就是普通的二叉树换成了二叉搜索树,正好帮助我们回顾二叉搜索树的特点。

    什么是二叉搜索树?

    二叉搜索树(英语:Binary Search Tree),也称为 二叉查找树、有序二叉树(Ordered Binary Tree)或排序二叉树(Sorted Binary Tree),是指一棵空树或者具有下列性质的二叉树:

    1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值
    2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值
    3. 任意节点的左、右子树也分别为二叉查找树
    4. 没有键值相等的节点。

    因此我们可以利用二叉搜索树的性质来简化236题的步骤。

    要寻找最近公共祖先,最直观的解法就是找到从根结点到结点 p、q的路径,然后在两条路径中寻找离根结点最远的相同结点,即为最近公共祖先。

    利用二叉搜索树来找到根结点到目标结点的路径的步骤如下:如果当前结点小于目标结点,则向右寻找;如果当前结点大于目标结点,则向左寻找;等于目标结点就直接返回。我们可用一个list来记录路径上的结点。

  • 代码一

    class Solution {
        public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
            List<TreeNode> list1 = search(root,p);
            List<TreeNode> list2 = search(root,q);
            int len = list1.size()>list2.size() ? list2.size() : list1.size();
            for(int i=len-1; i>=0; i--) {
                if(list1.get(i)==list2.get(i)) {
                    return list1.get(i);
                }
            }
            return null;
        }
    
        //利用二叉搜索树来查找从根结点到某个结点的路径
        public List<TreeNode> search(TreeNode root, TreeNode target) {
            List<TreeNode> res = new ArrayList<>();
            while(root != target) {
                res.add(root);
                //目标值大于当前结点,向右侧寻找
                if(root.val < target.val) {
                    root = root.right;
                }else {
                    //目标值小于当前结点,向左侧寻找
                    root = root.left;
                }
            }
            res.add(root);
            return res;
        }
    }
    
  • 分析二

    上述方法是两次遍历,实际上可以优化到一次遍历。

    236题已经提到,当前结点root为结点p和结点q的最近公共祖先,有三种情况:

    1. 结点p和结点q分别位于结点root的左右子树中,则root一定为pq的最近公共祖先;
    2. root是结点p,结点q在结点p的子树上,则结点ppq的最近公共祖先;
    3. root是结点q,结点p在结点q的子树上,则结点qpq的最近公共祖先;

    因此我们可以直接同时比较当前结点结点p、q的值,步骤如下:

    1. 如果大于结点p、q的值,说明最近公共祖先在当前结点的左侧;
    2. 如果小于结点p、q的值,说明最近公共祖先在当前结点的右侧;
    3. 若上述两种情况都不满足,说明结点p、q在当前结点的两侧或当前结点就是p、q中的一个,则当前结点就是最近公共祖先。
  • 代码二

    class Solution {
        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.left;
                }else if(root.val < p.val && root.val < q.val) {
                    //p,q结点在当前结点右侧
                    root = root.right;
                }else{
                    //在两侧或当前结点为p或q
                    return root;
                }
            }
            return null;
        }
    }
    

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

  • 题目描述:序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。

    请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构

  • 分析

    这是一个经典的二叉树题目。

    首先我们要知道如何将一个二叉树表示成一个字符串,有两个关键点:按照什么顺序,结点为空时怎样表示?

    第一个问题,我们可以用BFSDFS,BFS即对二叉树层序遍历,DFS即对二叉树前序遍历(前序遍历容易找到根结点);

    第二个问题,由于结点值都是整数,我们可以用#来代表空结点(当然其它能区别整数的字符也可以),这样我们就能完整的表示一个二叉树了。注意结点之间不要忘记用逗号隔开,如下图:

    二叉树的序列化

    反序列化时,我们要按照序列化时的顺序去解码,具体过程见代码。

  • 代码

    DFS

    /**
     * Definition for a binary tree node.
     * public class TreeNode {
     *     int val;
     *     TreeNode left;
     *     TreeNode right;
     *     TreeNode(int x) { val = x; }
     * }
     */
    public class Codec {
        // Encodes a tree to a single string.
        public String serialize(TreeNode root) {
            //DFS,前序遍历
            if(root == null) return "#";
            return root.val+","+serialize(root.left)+","+serialize(root.right);
        }
    
        // Decodes your encoded data to tree.
        public TreeNode deserialize(String data) {
            String[] datas = data.split(",");
            List<String> list = new LinkedList<>(Arrays.asList(datas));
    
            return decode(list);
        }
    
        public TreeNode decode(List<String> list) {
            //按照先序遍历反序列化
            if(list.get(0).equals("#")) {
                //表示结点为空,要移出列表,返回null
                list.remove(0);
                return null;
            }
            //根左右的顺序
            TreeNode root = new TreeNode(Integer.parseInt(list.get(0)));
            list.remove(0);
            root.left = decode(list);
            root.right = decode(list);
    
            return root;
        }
    }
    

    BFS

    public class Codec {
        // Encodes a tree to a single string.
        public String serialize(TreeNode root) {
            //BFS,层序遍历
            if(root == null) return "#";
            Queue<TreeNode> queue = new LinkedList<>();
            StringBuilder sb = new StringBuilder();
            //根结点入队
            queue.offer(root);
    
            while(!queue.isEmpty()) {
                TreeNode cur = queue.poll();
                if(cur == null) {
                    //当前结点为空,向字符串中添加"#"来代替空结点
                    sb.append("#,");
                }else {
                    //当前结点不为空,将当前值添加到字符串中
                    sb.append(cur.val+",");
                    //然后子结点入队
                    queue.offer(cur.left);
                    queue.offer(cur.right);
                }
            }
            return sb.toString();
        }
    
        // Decodes your encoded data to tree.
        public TreeNode deserialize(String data) {
            String[] nodes = data.split(",");
    
            return decode(nodes);
        }
    
        public TreeNode decode(String[] nodes) {
            if(nodes[0].equals("#")) {
                return null;
            }
            //按照层序遍历反序列化
            Queue<TreeNode> queue = new LinkedList<>();
            TreeNode root = new TreeNode(Integer.parseInt(nodes[0]));
            queue.offer(root);
            //左右子结点的下标
            int l = 1, r = 2;
            while(!queue.isEmpty()) {
                TreeNode cur = queue.poll();
                //构造其左右子结点
                if(!nodes[l].equals("#")) {
                    //左结点不为空时入队
                    TreeNode left = new TreeNode(Integer.parseInt(nodes[l]));
                    queue.offer(left);
                    cur.left = left;
                }
                if(!nodes[r].equals("#")) {
                    //右结点不为空时入队
                    TreeNode right = new TreeNode(Integer.parseInt(nodes[r]));
                    queue.offer(right);
                    cur.right = right;
                }
                //寻找队列中下一个结点的左右子结点
                l += 2;
                r += 2;
            }
    
            return root;
        }
    }
    

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值