二叉树的原理及前中后序、层序遍历的实现

1.二叉树

1.1 概念

一棵二叉树是结点的一个有限集合,该集合或者为空,或者是由一个根节点加上两棵别称为左子树和右子树的二叉树组成。
二叉树的特点:

  • 每个结点最多有两棵子树,即二叉树不存在度大于 2 的结点。
  • 二叉树的子树有左右之分,其子树的次序不能颠倒,因此二叉树是有序树。

1.2 二叉树的基本形态

图示:
在这里插入图片描述
可以从上图看到,从左到右,依次是空树,只有根节点的二叉树、只有左子树的二叉树、只有右子树的二叉树、节点的左右子树均存在

1.3 两种特殊的二叉树

  • 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是 2^k - 1,则它就是满二叉树。
    图示:
    在这里插入图片描述
  • 完全二叉树: 完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
    图示:
    在这里插入图片描述

1.4 二叉树的性质

  • 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有 2^i -1 (i>0)个结点
  • 若规定只有根节点的二叉树的深度为1,则深度为K的二叉树的最大结点数是 2^k - 1(k>=0)
  • 对任何一棵二叉树, 如果其叶结点个数为 n0, 度为2的非叶结点个数为 n2,则有n0=n2+1
  • 具有n个结点的完全二叉树的深度k为 log2(n+1)上取整
  • 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的顺序对所有节点从0开始编号,则对于序号为i的结点有:
    • 若i>0,双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
    • 若2i+1<n,左孩子序号:2i+1,否则无左孩子
    • 若2i+2<n,右孩子序号:2i+2,否则无右孩子

推导:

n:代表一颗二叉树中的所有节点个数
n1:代表一颗二叉树中,度为1 的节点个数
e:代表一颗二叉树中的所有边的个数
从节点个数角度:
n = n0+n1+n2;
从边的角度:
从节点上面的边:
e = n-1
从节点下面的边:
e = 2 * n2 + 1 * n1+0 * n0 = 2 * n2+n1
推导出:n0 = n2+1

对于一颗二叉树中共n个节点:

  • 节点个数是偶数,则1个节点只有左孩子,0个有右孩子;个数为奇数,则0个节点只有左孩子,0个只有右孩子。
  • 叶子节点:先看对于满二叉树缺了多少节点,然后最后一层的满节点数减去缺的节点数加上缺的节点/2即为叶子节点
  • 非叶子节点:节点总数减去叶子节点个数

1.5 二叉树的存储

二叉树的存储结构分为:顺序存储和类似于链表的链式存储。
二叉树的链式存储是通过一个一个的节点引用起来的,常见的表示方式有二叉和三叉表示方式,具体如下:

// 孩子表示法
class Node {
 int val; // 数据域
 Node left; // 左孩子的引用,常常代表左孩子为根的整棵左子树
 Node right; // 右孩子的引用,常常代表右孩子为根的整棵右子树
}
// 孩子双亲表示法
class Node {
 int val; // 数据域
 Node left; // 左孩子的引用,常常代表左孩子为根的整棵左子树
 Node right; // 右孩子的引用,常常代表右孩子为根的整棵右子树
 Node parent; // 当前节点的根节点
}

主要采用孩子表示法来构建二叉树

1.6 二叉树的基本操作

首先创建一个节点类:

//二叉树节点类
public class TreeNode {
    public int val;
    public TreeNode left;
    public TreeNode right;

    public TreeNode(int val) {
        this.val = val;
    }
}
1.6.1 二叉树的遍历
1.6.1.1 前序遍历
  • 利用递归的思想:先根,再左子树,再右子树
    代码示例:
 /**
     * 前序遍历
     * @param root
     */
    public static void preTraversal(TreeNode root){
        if(root!=null){
        	//以字符形式打印
            System.out.printf("%C",root.val);
            preTraversal(root.left);
            preTraversal(root.right);
        }
    }
  • 非递归前序遍历:需要用到栈
    代码示例:
 public static void preOrderTraversal(TreeNode root){
        Deque<TreeNode> stack = new LinkedList<>();
        TreeNode cur = root;
        while(!stack.isEmpty() || cur!=null){
            while(cur!=null){
                System.out.printf("%C ",cur.val);
                stack.push(cur);
                cur = cur.left;
            }
            //当cur 为空时,出栈让cur等于栈顶元素的right 右子树继续走
            TreeNode top = stack.pop();
            cur = top.right;
        }
    }
1.6.1.2 中序遍历
  • 利用递归的思想:先左子树,再根,再右子树
    代码示例:

    /**
     * 中序遍历
     * @param root
     */
    public static void inTraversal(TreeNode root){
        if(root!=null){
            inTraversal(root.left);
            System.out.printf("%C ",root.val);
            inTraversal(root.right);
        }

    }
  • 非递归中序遍历:需要一个栈
    代码示例:
// 非递归中序遍历
    public static void inOrderTraversal(TreeNode root){
        Deque<TreeNode> stack = new LinkedList<>();
        TreeNode cur = root;
        while(!stack.isEmpty() || cur!=null){
            while(cur!=null){
                //第一次
                stack.push(cur);
                cur = cur.left;
            }
            //当cur 为空时,出栈让cur等于栈顶元素的right 右子树继续走
            TreeNode top = stack.pop();
            //第二次
            System.out.printf("%C ",top.val);
            cur = top.right;
        }
    }
1.6.1.3 后序遍历
  • 利用递归的思想:先左子树,再右子树,再根
    代码示例:
/**
     * 后序遍历
     * @param root
     */
    public static void postTraversal(TreeNode root){
        if(root!=null){
            postTraversal(root.left);
            postTraversal(root.right);
            System.out.printf("%C ",root.val);
        }
    }
  • 非递归后序遍历:需要用到栈
    代码示例:
    /**
     * 非递归后序遍历
     *   需要记录上一次遍历出来的节点
     */

    public static void postOrderTraversal(TreeNode root){
        Deque<TreeNode> stack = new LinkedList<>();
        TreeNode cur = root;
        TreeNode last = null; //记录上一次遍历出来的节点
        while(!stack.isEmpty() || cur!=null){
            while(cur!=null){
                //第一次经过
                stack.push(cur);
                cur = cur.left;
            }
            //当cur 为空时,查看栈顶元素 右子树继续走
            TreeNode top = stack.peek();
            if(top.right==null){
                //第二次经过
                //从左子树回来,但由于右子树为空,可以看作从右子树回来 可以视为第三次
                stack.pop();
                last = top;
                System.out.println(top.val);
            }else if(top.right==last){
                //第三次经过
                //说明从右子树回来
                stack.pop();
                last = top;
            }else{
                //说明从左子树回来 第二次经过
                cur = top.right;
            }

        }
    }
1.6.2 二叉树的基本操作
1.6.2.1 求所有节点个数
  • 遍历思路:
    代码示例:

    /**
     * 求树中的所有节点个数
     * 遍历思路
     */
    private static int n = 0;
    public static void preOrder_1(TreeNode root){
        if(root!=null){
            n++;
            preOrder_1(root.left);
            preOrder_1(root.right);
        }
    }
    public static int getSize_1(TreeNode root){
        n = 0;

        preOrder_1(root);
        return n;
    }
  • 子问题思路:
    代码示例:
/**
     * 子问题思路
     * 以root为根的节点个数  根的个数加上 以root.left 为根的节点个数加上 root.right 为根的节点个数
     * root 为空 则为0
     */
    public static int getSize_2(TreeNode root){

        if(root!=null){
            return 1+getSize_2(root.left)+getSize_2(root.right);
        }
        return 0;
    }
1.6.2.2 求叶子节点个数
  • 遍历思路:
    代码示例:
 /**
     * 遍历思路
     * 求叶子节点个数
     */
    private static int n = 0;
    //利用前序遍历
    public static void preOrder_2(TreeNode root){
        if(root!=null){
            if(root.left==null && root.right==null){
                n++;
            }
            preOrder_2(root.left);
            preOrder_2(root.right);
        }
    }
    public static int leafSize_1(TreeNode root){
        n=0;
        preOrder_2(root);
        return n;
    }
  • 子问题思路:
    代码示例:
/**
     * 子问题思路
     *
     */
    public static int leafSize_2(TreeNode root){

       if(root!=null){
           //只有一个节点的树,求叶子节点个数
           if(root.left==null && root.right==null){
              return  1;
           }
           //左子树 加右子树叶子节点个数
           return leafSize_2(root.left)+leafSize_2(root.right);
       }
       //空树
       return 0;
    }
1.6.2.3 求第k层节点个数

利用递归思想:
求第k层节点个数即为求左子树第k-1层节点个数+右子树第k-1层节点个数,当二叉树只有一层,此时节点个数为1
代码示例:

 /**
     * 遍历思路求第k层 节点个数
     * 求左子树第k-1层的节点个数+右子树第k-1层的节点个数
     * 当k==1时,返回 1
     * @param root
     * @return
     */
    public static int getKLevelSize(TreeNode root,int k){
        //root代表空树
        //只有一个节点
        //其他情况
        if(root==null){
            return 0;
        }else if(k==1){
            return 1;
        }else{
            return getKLevelSize(root.left,k-1)+getKLevelSize(root.right,k-1);
        }
    }
1.6.2.4 获取二叉树的高度

思路:返回一棵树的左子树的高度与右子树的高度比较,其中的大值加上根的高度即1
代码示例:

 /**
     *  获取二叉树的高度
     */

    public static int getHeight(TreeNode root){
        if(root==null){
            return 0;
        }

        int leftHeight = getHeight(root.left);
        int rightHeight = getHeight(root.right);

        return leftHeight>rightHeight?leftHeight+1:rightHeight+1;
    }
1.6.2.5 查找val所在值的节点

思路:二叉树为空返回false,二叉树只有一个节点比较值。在左子树找,找到返回,没找到在右子树找。
代码示例:

/**
     * 查找值所在的节点
     * @param root
     * @param val
     * @return
     */
    public static boolean contains(TreeNode root,int val){
        if(root==null){
            return false;
        }
        if(root.val == val){
            return true;
        }
        if(contains(root.left,val)){
            return true;
        }
        return contains(root.right,val);
    }
1.6.2.6 查找节点是否存在于二叉树

思路:与查找val所在值的节点思路一致,不过需要比较的是两个节点的引用是否相等,而不是值
代码示例:

	/**
     * 查找节点是否存在于二叉树
     */
    public static boolean contains(TreeNode root,TreeNode node){
        if(root==null){
            return false;
        }

        if(root==node){
            return true;
        }

        boolean leftContains = contains(root.left,node);
        if(leftContains){
            return true;
        }
        return contains(root.right,node);

    }
1.6.2.7 利用带空节点的先序遍历 ,创建一颗二叉树

思路1: 使用两个list

  • 情况1:如果前序序列为空,剩余序列也是空的
  • 情况2:取出的根的值==’#’ 空树 去除第一个字符作为剩余
  • 情况3:
    • 1.利用根的值,构建根节点[根][左子树][右子树][剩余部分]
    • 2.递归调用 将[左子树][右子树][剩余部分] 作为输入,构建输出 左子树/ [右子树][剩余部分] 作为构建左子树的剩余部分
    • 3.递归调用 将[右子树][剩余部分] 构建二叉树
      输出:右子树 / [剩余部分](构建完整树的剩余部分) 作为构建右子树时的剩余部分

代码示例:

public static TreeNode buildTree(List<Character> in,List<Character> out){
        if(in.isEmpty()){
            //没有序列
            return null;
        }

        //in不为空
        char rootVal = in.remove(0);
        if(rootVal == '#'){
            //剩下的就是in 去除第一个元素(#)
            //遇到#一定是空树
            out.addAll(in);
            return null;
        }

        // 当rootVal 一定不为#,构建不同的节点和以该节点为根的树
        TreeNode root = new TreeNode(rootVal);

        //这里的in 由于调用过remove,不包括第一个元素
        List<Character> rightOut = new ArrayList<>();

        TreeNode left = buildTree(in,rightOut);

        TreeNode right = buildTree(rightOut,out);

        root.left = left;
        root.right = right;

        return root;
    }

思路2:使用下标控制
代码示例:

	 private  static int i=0;
    public static TreeNode buildTree(List<Character> pre){
        if(pre.isEmpty()){
            //没有序列
            return null;
        }
        if(pre.get(i)=='#'){
            return null;
        }else{
            TreeNode root = new TreeNode(pre.get(i));
            i++;
            TreeNode left = buildTree(pre);
            i++;
            TreeNode right = buildTree(pre);
            root.left = left;
            root.right =right;
            return root;
        }
    }

1.7 二叉树的层序遍历

设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。层序遍历需要用到队列

  • 不带层级的层序遍历:
    图示:
    在这里插入图片描述

代码示例:

 /**
     * 层序遍历 广度优先(采用队列)
      */

    public static void levelOrderTraversal(TreeNode root){
        if(root==null){
            return;
        }
        Queue<TreeNode> queue = new LinkedList<>();

        queue.add(root);

        while(!queue.isEmpty()){
            TreeNode r = queue.remove();
            System.out.printf("%C ",r.val);

            if(r.left!=null){
                queue.add(r.left);
            }
            if(r.right!=null){
                queue.add(r.right);
            }
        }
    }
  • 带层级的层序遍历
    思路:使用一个类封装节点类型和int类型的层级,每次入队将这个类添加进去,同时对应层级加1
    代码示例:
 //带层级的 节点类
    public static class NL{
        public TreeNode node;
        public int level;

        public NL(TreeNode node, int level) {
            this.node = node;
            this.level = level;
        }
    }

    /**
     * 带层级的遍历
     * @param root
     */
    public static void levelOrderTraversal_withLevel(TreeNode root){
        if(root==null){
            return;
        }
        Queue<NL> queue = new LinkedList<>();

        queue.add(new NL(root,1));

        while(!queue.isEmpty()){
            NL l = queue.remove();
            System.out.printf("层级: %d %C ",l.level,l.node.val);

            if(l.node.left!=null){
                queue.add(new NL(l.node.left,l.level+1));
            }
            if(l.node.right!=null){
                queue.add(new NL(l.node.right,l.level+1));
            }
        }
    }

1.8 小结

  • 遍历:
    前、中、后序的遍历(深度优先——栈)。递归、非递归
    层序遍历(广度优先——队列)

  • 遍历思路:(求节点个数、求叶子节点个数、搜索树变有序链表)(前中后 OR 层序)

  • 汇总的思路/子问题思路(前中后序)

    • 把树看作三部分(根+左子树+右子树)
    • 左右子树使用小规模的思路解决
    • 层序遍历的变形
  • 递归方法的理解:不做递归展开
    递归:将相同子问题,较大规模问题转换为较小规模问题,直到规模小到一定程度(空树/一个节点的树)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值