算法训练--二叉树(1)

210 阅读6分钟

基本概念

二叉树的种类

  • 满二叉树与完全二叉树

    • 满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树

      img

      这棵二叉树为满二叉树,也可以说深度为k,有2^k-1个节点的二叉树

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

      img

      之前我们刚刚讲过优先级队列其实是一个堆,堆就是一棵完全二叉树,同时保证父子节点的顺序关系

二叉搜索树

  • 前面介绍的树,都没有数值的,而二叉搜索树是有数值的了,二叉搜索树是一个有序树

    • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
    • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
    • 它的左、右子树也分别为二叉排序树

    img

平衡二叉搜索树

  • 平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树

    img

二叉树的遍历

  • 二叉树主要有两种遍历方式:

    • 深度优先遍历(DFS):先往深走,遇到叶子节点再往回走;
    • 广度优先遍历(BFS)一层一层的去遍历;广度优先遍历的实现一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能一层一层的来遍历二叉树
  • 那么从深度优先遍历和广度优先遍历进一步拓展,才有如下遍历方式:

    • 深度优先遍历
      • 前序遍历(递归法,迭代法)
      • 中序遍历(递归法,迭代法)
      • 后序遍历(递归法,迭代法)
    • 广度优先遍历
      • 层次遍历(迭代法)
  • 三种遍历方式

    image.png

二叉树的定义(Java版)

  • LeetCode帮我们定义好了,但是ACM或者面试手写时,可能需要自己定义

    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;
      }
    }
    

二叉树的遍历方式

二叉树的递归遍历

  • 我们要通过简单题目把方法论确定下来,有了方法论,后面才能应付复杂的递归
  • 递归三要素
    • 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型;
    • 确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出;
    • 确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程

144. 二叉树的前序遍历

  • 题解

    前序遍历:根左右

    /**
     * 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<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){
                return;
            }
            res.add(root.val);
            preorder(root.left,res);
            preorder(root.right,res);
        }
    }
    

94. 二叉树的中序遍历

  • 题解

    中序遍历:左根右

    /**
     * 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<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){
                return;
            }
            inorder(root.left,res);
            res.add(root.val);
            inorder(root.right,res);
        }
    }
    

145. 二叉树的后序遍历

  • 题解

    后序遍历:左右跟

    /**
     * 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<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){
                return;
            }
            postorder(root.left,res);
            postorder(root.right,res);
            res.add(root.val);
        }
    }
    

589. N 叉树的前序遍历

  • 题目描述

    image.png

  • 题解:递归

    class Solution {
        List<Integer> res=new ArrayList<>();
        public List<Integer> preorder(Node root) {
            if(root==null) return res;
            res.add(root.val);
            for(Node child:root.children){
                preorder(child);
            }
            return res;
        }
    }
    

590. N 叉树的后序遍历

  • 题解

    class Solution {
        List<Integer> res=new ArrayList<>();
        public List<Integer> postorder(Node root) {
            if(root==null) return res;
            for(Node child:root.children){
                postorder(child);
            }
            res.add(root.val);
            return res;
        }
    }
    

二叉树的迭代遍历

  • 为什么可以用**迭代法(非递归的方式)**来实现二叉树的前后中序遍历呢?

    递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因;因此我们用栈也可以是实现二叉树的前后中序遍历

144. 二叉树的前序遍历-迭代法

  • 题解

    二叉树前序遍历(迭代法)

    class Solution {
        public List<Integer> preorderTraversal(TreeNode root) {
            //前序遍历:根左右  入栈顺序:根右左
            List<Integer> res=new ArrayList<>();
            Stack<TreeNode> stack=new Stack<>();
            if(root==null){
                return res;
            }
            stack.push(root);
            while(!stack.isEmpty()){
                TreeNode node=stack.pop();
                res.add(node.val);
                if(node.right!=null){
                    stack.push(node.right);
                }
                if(node.left!=null){
                    stack.push(node.left);
                }
            }
            return res;
        }
    }
    

    此时是不是想改一点前序遍历代码顺序就把中序遍历搞出来了?但接下来,再用迭代法写中序遍历的时候,会发现套路又不一样了,目前的前序遍历的逻辑无法直接应用到中序遍历

94. 二叉树的中序遍历-迭代法

  • 题解:在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素

    二叉树中序遍历(迭代法)

    class Solution {
        public List<Integer> inorderTraversal(TreeNode root) {
            //中序遍历:左-根-右  入栈顺序:左-右
            List<Integer> res=new ArrayList<>();
            if(root==null){
                return res;
            }
            Stack<TreeNode> stack=new Stack<>();
            TreeNode cur=root;
            while(cur!=null || !stack.isEmpty()){
                if(cur!=null){
                    stack.push(cur);
                    cur=cur.left;
                }else{
                    cur=stack.pop();
                    res.add(cur.val);
                    cur=cur.right;
                }
            }
            return res;
        }
    }
    

145. 二叉树的后序遍历-迭代法

  • 题解:后序遍历迭代法只需要前序遍历迭代法的代码稍作修改就可以了

    最后反转结果

    class Solution {
        public List<Integer> postorderTraversal(TreeNode root) {
            //后序遍历:左右根  入栈顺序:根左右 出栈顺序:根右左 最后反转结果
            List<Integer> res=new ArrayList<>();
            if(root==null){
                return res;
            }
            Stack<TreeNode> stack=new Stack<>();
            stack.push(root);
            while(!stack.isEmpty()){
                TreeNode node=stack.pop();
                res.add(node.val);
                if(node.left!=null){
                    stack.push(node.left);
                }
                if(node.right!=null){
                    stack.push(node.right);
                }
            }
            Collections.reverse(res);
            return res;
        }
    }
    

二叉树的统一迭代法

  • 我们发现迭代法实现的先中后序,其实风格也不是那么统一,除了先序和后序,有关联,中序完全就是另一个风格了,一会用栈遍历,一会又用指针来遍历
  • 迭代法无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记,如何标记呢,就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法也可以叫做标记法

144. 二叉树的前序遍历

  • 题解

    /**
    	前序遍历:根左右  统一迭代法:右左根
    */
    class Solution {
        public List<Integer> preorderTraversal(TreeNode root) {
            List<Integer> res=new ArrayList<>();
            Stack<TreeNode> stack=new Stack<>();
            if(root!=null) stack.push(root);
            while(!stack.isEmpty()){
                TreeNode node=stack.peek();
                if(node!=null){
                  	//将该节点弹出,避免重复操作,下面再将右左根节点分别添加到栈中
                    stack.pop();
                    if(node.right!=null) stack.push(node.right);
                    if(node.left!=null) stack.push(node.left);
                  	//添加根节点
                  	//根节点访问过,但是还没有处理,加入空节点做为标记
                    stack.push(node);
                    stack.push(null);
                }else{
                  	//只有遇到空节点,才将下一个节点放入结果集
                    stack.pop();
                    node=stack.peek();
                    stack.pop();
                    res.add(node.val);
                }
            }
            return res;
        }
    }
    

94. 二叉树的中序遍历-统一迭代

  • 题解:

    /**
    	中序遍历:左根右     统一迭代法:右根左
    */
    class Solution {
        public List<Integer> inorderTraversal(TreeNode root) {
            List<Integer> res=new ArrayList<>();
            Stack<TreeNode> stack=new Stack<>();
            if(root!=null) stack.push(root);
            while(!stack.isEmpty()){
                TreeNode node=stack.peek();
                if(node!=null){
                  	//将该节点弹出,避免重复操作,下面再将右根左节点分别添加到栈中
                    stack.pop();
                  	//添加右节点(空节点不入栈)
                    if(node.right!=null) stack.push(node.right);
                  	//添加根节点
                    stack.push(node);
                  	//根节点访问过,但是还没有处理,加入空节点做为标记
                    stack.push(null);
                  	//添加左节点(空节点不入栈)
                    if(node.left!=null) stack.push(node.left);
                }else{
                  	//只有遇到空节点,才将下一个节点放入结果集
                  	//空节点弹出
                    stack.pop();
                  	//下一个节点
                    node=stack.peek();
                    stack.pop();
                    res.add(node.val);
                }
            }
            return res;
        }
    }
    

145. 二叉树的后序遍历-统一迭代法

  • 题解

    /**
    	后序遍历:左右根    统一迭代法:根右左
    */
    class Solution {
        public List<Integer> postorderTraversal(TreeNode root) {
            List<Integer> res=new ArrayList<>();
            Stack<TreeNode> stack=new Stack<>();
            if(root!=null) stack.push(root);
            while(!stack.isEmpty()){
                TreeNode node=stack.peek();
                if(node!=null){
                    stack.pop();
                  	//将该节点弹出,避免重复操作,下面再将根右左节点分别添加到栈中
                    stack.push(node);
                    stack.push(null);
                    if(node.right!=null) stack.push(node.right);
                    if(node.left!=null) stack.push(node.left);
                }else{
                  	//只有遇到空节点,才将下一个节点放入结果集
                  	//弹出空节点
                    stack.pop();
                    node=stack.peek();
                    stack.pop();
                    res.add(node.val);
                }
            }
            return res;
        }
    }
    

二叉树的层序遍历

102. 二叉树的层序遍历

  • 题目描述

    image.png

  • 题解

    需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而是用栈先进后出适合模拟深度优先遍历也就是递归的逻辑

    102二叉树的层序遍历

    /**
    	广度优先遍历 BFS
    */
    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 len=queue.size();
                List<Integer> temp=new ArrayList<>();
                while(len>0){
                    TreeNode node=queue.poll();
                    temp.add(node.val);
                    if(node.left!=null) queue.offer(node.left);
                    if(node.right!=null) queue.offer(node.right);
                    len--;
                }
                res.add(temp);
            }
            return res;
        }
    }
    

107. 二叉树的层序遍历 II

  • 题目描述

    image.png

  • 题解

     /**
    		队列 迭代
      	层序遍历 最后翻转数组
     */
    class Solution {
        public List<List<Integer>> levelOrderBottom(TreeNode root) {
             List<List<Integer>> list=new ArrayList<>();
             if(root==null){
                 return list;
             }
             Queue<TreeNode> queue=new LinkedList<>();
             queue.offer(root);
             while(!queue.isEmpty()){
                 int len=queue.size();
                 List<Integer> temp=new ArrayList<>();
                 while(len>0){
                    TreeNode node=queue.poll();
                    temp.add(node.val);
                    if(node.left!=null) queue.offer(node.left);
                    if(node.right!=null) queue.offer(node.right);
                    len--;
                 }
                 list.add(temp);
             }
             List<List<Integer>> res=new ArrayList<>();
             for(int i=list.size()-1;i>=0;i--){
                 res.add(list.get(i));
             }
             return res;
        }
    }
    

199. 二叉树的右视图

  • 题目描述

    image.png

  • 题解:层序遍历的时候,判断是否遍历到单层的最后面的元素,如果是,就放进result数组中,随后返回result就可以了

    /**
    	每次返回每层的最后一个元素
    */
    class Solution {
        public List<Integer> rightSideView(TreeNode root) {
            List<Integer> res=new ArrayList<>();
            if(root==null) return res;
            Queue<TreeNode> queue=new LinkedList<>();
            queue.offer(root);
            while(!queue.isEmpty()){
                int len=queue.size();
                for(int i=0;i<len;i++){
                    TreeNode node=queue.poll();
                    if(node.left!=null) queue.offer(node.left);
                    if(node.right!=null) queue.offer(node.right);
                    if(i==len-1){
                        res.add(node.val);
                    }
                }
            }
            return res;
        }
    }
    

637. 二叉树的层平均值

  • 题目描述

    image.png

  • 题解

    /**
    	层序遍历 每层求和 再求平均值
    */
    class Solution {
        public List<Double> averageOfLevels(TreeNode root) {
            List<Double> res=new ArrayList<>();
            if(root==null) return res;
            Deque<TreeNode> queue=new LinkedList<>();
            queue.offer(root);
            while(!queue.isEmpty()){
                int len=queue.size();
                Double sum=0.00;
                for(int i=0;i<len;i++){
                    TreeNode node=queue.poll();
                    sum+=node.val;
                    if(node.left!=null) queue.offer(node.left);
                    if(node.right!=null) queue.offer(node.right);
                    
                }
                res.add(sum/len);
            }
            return res;
        }
    }
    

429. N 叉树的层序遍历

  • 题目描述

    image.png

  • 题解

    /**
    	层序遍历 左右子节点 变为 孩子节点
    */
    class Solution {
        public List<List<Integer>> levelOrder(Node root) {
           List<List<Integer>> res=new ArrayList<>();
           if(root==null) return res;
           Queue<Node> queue=new LinkedList<>();
           queue.offer(root);
           while(!queue.isEmpty()){
               int size=queue.size();
               List<Integer> temp=new ArrayList<>();
               for(int i=0;i<size;i++){
                   Node node = queue.poll();
                   temp.add(node.val);
                   if(node.children==null || node.children.size()==0){
                       continue;
                   }
                   for(Node child:node.children){
                       queue.offer(child);
                   }
               }
               res.add(temp);
           } 
           return res;
        }
    }
    

515. 在每个树行中找最大值

  • 题目描述

    image.png

  • 题解

    /**
    	层序遍历 取每一层的最大值
    */
    class Solution {
        public List<Integer> largestValues(TreeNode root) {
            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();
                int max=Integer.MIN_VALUE;
                for(int i=0;i<size;i++){
                    TreeNode node=queue.poll();
                    max=Math.max(max,node.val);
                    if(node.left!=null) queue.offer(node.left);
                    if(node.right!=null) queue.offer(node.right);
                }
                res.add(max);
            }
            return res;
        }
    }
    

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

  • 题目描述

    image.png

    image.png

  • 题解:本题依然是层序遍历,只不过在单层遍历的时候记录一下本层的头部节点,然后在遍历的时候让前一个节点指向本节点就可以了

    class Solution {
        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();
                Node cur=root;
                Node next=root;
                for(int i=0;i<size;i++){
                    if(i==0){
                        //记录本层头结点
                        cur=queue.poll();
                    }else{
                        next=queue.poll();
                        //指向右侧节点
                        cur.next=next;
                        //后移
                        cur=next;
                    }
                    if(cur.left!=null) queue.offer(cur.left);
                    if(cur.right!=null) queue.offer(cur.right);
                }
            }
            return root;
        }
    }
    

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

  • 题目描述

    image.png

    image.png

  • 题解:这道题目说是二叉树,但116题目说是完整二叉树,其实没有任何差别,一样的代码一样的逻辑一样的味道

    class Solution {
        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();
                Node cur=root;
                Node next=root;
                for(int i=0;i<size;i++){
                    if(i==0){
                        cur=queue.poll();
                    }else{
                        next=queue.poll();
                        cur.next=next;
                        cur=next;
                    }
                    if(cur.left!=null) queue.offer(cur.left);
                    if(cur.right!=null) queue.offer(cur.right);
                }
            }
            return root;
        }
    }
    

104. 二叉树的最大深度

  • 题目描述

    image.png

  • 题解

    /**
    	层序遍历 层数就是最大深度
    */
    class Solution {
        public int maxDepth(TreeNode root) {
            int level=0;
            if(root==null) return level;
            Queue<TreeNode> queue=new LinkedList<>();
            queue.offer(root);
            while(!queue.isEmpty()){
                int size=queue.size();
                while(size>0){
                    TreeNode node=queue.poll();
                    if(node.left!=null) queue.offer(node.left);
                    if(node.right!=null) queue.offer(node.right);
                    size--;
                }
                level++;
            }
            return level;
        }
    }
    

111. 二叉树的最小深度

  • 题目描述

    image.png

  • 题解:需要注意的是,只有当左右孩子都为空的时候,才说明遍历的最低点了。如果其中一个孩子为空则不是最低点

    class Solution {
        public int minDepth(TreeNode root) {
            int minLevel=0;
            if(root==null) return minLevel;
            Queue<TreeNode> queue=new LinkedList<>();
            queue.offer(root);
            while(!queue.isEmpty()){
                int size=queue.size();
                minLevel++;
                while(size>0){
                    TreeNode node=queue.poll();
                    if(node.left!=null) queue.offer(node.left);
                    if(node.right!=null) queue.offer(node.right);
                    if(node.left==null && node.right==null){
                        return minLevel;
                    }
                    size--;
                }
            }
            return minLevel;
        }
    }