Java数据结构——二叉树

目录

关于树的基础概念

二叉树

二叉树的遍历


问:为什么会存在树结构?

答:树具有高效的查找与搜索语义

如 OS中的文件系统就是基于树结构进行文件管理的

若当前OS中所有文件都放在一个”目录“下,若当前操作系统有1亿个文件,最坏情况遍历1亿次才能找到特定元素。而基于树结构的文件管理,只需遍历logN次

关于树的基础概念

线性数据结构——线性表,元素之间逻辑上一个挨着一个,呈直线排列,e.g. 数组 链表 栈 队列 字符串

树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看 起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:

  • 一个特殊的节点,称为根节点,根节点没有前驱节点

  • 除根节点外,其余节点被分成M(M > 0)个互不相交的集合T1、T2、......、Tm,其中每一个集合 Ti (1 <= i <= m) 又是一棵与树类似的子树。每棵子树的根节点有且只有一个前驱,可以有0个或多个后继

  • 树是递归定义的。

从根节点除法分成的M个子集,彼此之间不能相交,若相交就不是树

结论:

  1. 子树不相交

  2. 除了根节点没有父节点外,每个结点有且仅有一个父节点

  3. 树、边的个数x和树中节点的个数n,x=n-1(每个结点都只有一个父节点,只有根节点没有父节点)

  4. 节点的度:该节点中包含子树的个数

    树的度:树中最大的节点的度

  5. 叶子节点:度为0的结点

    非叶子节点:度不为0的节点

  6. 根节点:没有父节点的节点,树中有且仅有一个

  7. 节点的层次(高度):从根节点开始计算,到该节点的层数

    树的高度:当前树中最大的节点层次

二叉树

树中节点最大的度为2的树,即在二叉树中,一个节点最多有两颗子树,二叉树节点的度<=2;子树有左右之分,左右的顺序不能颠倒。

二叉树常考的性质:

  1. 在深度为k的二叉树中,最多存在2^k-1个节点

  2. 在第k层,该层最多有2^(k-1)个节点

  3. 由于任意二叉树都满足节点个数n和边长x具备x=n-1 ==> 在二叉树中,度为2的节点和度为0的节点有以下关系: n0 = n2 + 1 (度为0的节点总比度为2的节点多一个)

常见的二叉树:

  1. 满二叉树: 每一个层的结点数都是最大值。也就是说,如果 一个二叉树的层数为K,且结点总数是2^k-1,则它就是满二叉树。

  2. 完全二叉树: 完全二叉树的节点编号和满二叉树完全一致。满二叉树是一种特殊的完全二叉树(从视觉上看,完全二叉树就是满二叉树缺了一个”右下角“)

    (完全二叉树只可能在最深的一层节点没有拉满,且这一层的节点均靠左排列)

    在完全二叉树中不存在只有右子树而没有左子树的结点

    在完全二叉树中,若存在度为1的节点,这个节点必然只有左子树没有右子树,且这个节点有且仅有一个

    堆:优先级队列,其实就是一个完全二叉树

    完全二叉树是效率很高的数据结构。对于深度为K的,有n 个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全 二叉树。

    完全二叉树的编号问题:

    1). 当根节点从1开始编号,若任意一个节点x,既存在子树也有父节点,则该节点的父节点编号为x/2,左子树的编号为2x,右子树的编号为2x+1

    2). 当根节点从0开始编号,”堆“就是基于数组实现的完全二叉树。此时,若任意一个节点x,既存在子树也有父节点,则该节点的父节点编号为(x-1)/2,左子树的编号为2x+1,右子树的编号为2x+2

        3. 二分搜索树(BST):节点的值之间有一个大小关系,左子树节点值<根节点值<右子树节点值

        若在BST中查找一个元素,其实讲就是二分查找

        4. 平衡二叉树:该树中任意一个节点的左右子树高度差都<=1

                AVL --> 严格平衡BST

                RBTree --> “黑节点”严格平衡的树

二叉树的遍历

是所有二叉树的基础操作,其实所有二叉树问题的解决思路都是遍历问题的衍生

遍历:按照一定的顺序访问这个集合的所有元素,做到不重复,不遗漏

对二叉树来说有四种遍历方式

深度优先遍历DFS:

  1. 前序遍历(先序遍历):(根左右)先访问根节点,然后递归访问左子树,再递归访问右子树

    第一次走到根节点就能”访问“

  2. 中序遍历:(左根右)先递归访问左子树,再访问根节点,最后递归访问右子树

    第二次走到根节点才能”访问“

  3. 后序遍历:(左右根)先递归访问左子树,再递归访问右子树,最后访问根节点

    第三次走到根节点才能”访问“

特点:

  1. 先序遍历的第一个节点一定是根节点

  2. 中序遍历根节点左侧为左子树遍历结果,右侧为右子树遍历结果

  3. 后续遍历结果镜像翻转后结果为根右左

注意:递归应掌握本质问题,就是这个方法能干的事,千万别纠结到底内部怎么运行的

 /**
     * 传入一颗树的根节点,递归打印先序遍历结果
     */
    public void preOrder(TreeNode<E> root) {
        // 当前树为空
        if (root == null) {
            return;
        }
        System.out.print(root.val + " ");
        preOrder(root.left);
        preOrder(root.right);
    }

    /**
     * 传入一颗树的根节点,递归打印中序遍历结果
     */
    public void midOrder(TreeNode<E> root) {
        // 当前树为空
        if (root == null) {
            return;
        }
        midOrder(root.left);
        System.out.print(root.val + " ");
        midOrder(root.right);
    }

    /**
     * 传入一颗树的根节点,递归打印后序遍历结果
     */
    public void postOrder(TreeNode<E> root) {
        // 当前树为空
        if (root == null) {
            return;
        }
        postOrder(root.left);
        postOrder(root.right);
        System.out.print(root.val + " ");
    }


    /**
     * 迭代的先序遍历
     * @param root
     * @return
     */
    public List<E> preIteration(TreeNode<E> root){
        // 记住极端情况判断!!!!
        // 存放先序遍历输出顺序
        List<E> res = new ArrayList<>();
        // 树为空
        if(root==null){
            return res;
        }

        Deque<TreeNode> stack = new LinkedList<>();
        stack.push(root);
        TreeNode cur = null; // 可删
        while (!stack.isEmpty()){
            cur = stack.pop(); // 变为TreeNode cur = stack.pop();
            res.add((E)cur.val);
            if(cur.right!=null){
                stack.push(cur.right);
            }
            if(cur.left!=null){
                stack.push(cur.left);
            }
        }
        return res;
    }

    /**
     * 迭代的中序遍历
     * @param root
     * @return
     */
    public List<E> midIteration(TreeNode<E> root){
        // 记住极端情况判断!!!!
        List<E> res = new ArrayList<>();
        // 树为空
        if(root==null){
            return res;
        }
        Deque<TreeNode> stack = new LinkedList<>();
        TreeNode cur = root;
        // 循环开始时,栈中没有任何元素,因此需要用cur不空保证程序可以进循环
        while(cur!=null || !stack.isEmpty()){
            // 左
            while (cur!=null){
                stack.push(cur);
                cur = cur.left;
            }
            // 中 或 没有左子树的最左节点
            cur = stack.pop();
            res.add((E)cur.val);
            // 右:cur没有右子树时cur.right为null,下一轮循环不检查左边,直接取栈顶元素即cur的父节点
            // 有右子树时cur.right不空,开始下一轮循环按左中右顺序中序遍历右子树
            cur = cur.right;
        }
        return res;
    }

    /**
     * 迭代的后序遍历
     * @param root
     * @return
     */
    public List<E> postIteration(TreeNode<E> root){
        // 记住极端情况判断!!!!
        List<E> res = new ArrayList<>();
        // 树为空
        if(root==null){
            return res;
        }
        Deque<TreeNode> stack = new LinkedList<>();
        TreeNode cur = root;
        TreeNode pre = null;
        // 循环开始时,栈中没有任何元素,因此需要用cur不空保证程序可以进循环
        while(cur!=null || !stack.isEmpty()){
            while(cur!=null){
                stack.push(cur);
                cur = cur.left;
            }
            // 中
            cur = stack.pop();
            // 有无右子树
            if(cur.right ==null || pre == cur.right){
                // cur.right!=null: 左子树已经处理过,且无右子树,因此可以直接输出
                // pre == cur.right: 左子树已经处理过,且右子树也已经处理过,因此可以直接输出
                res.add((E)cur.val);
                pre = cur;
                cur = null; // 避免下一次循环再次处理cur
            }else {
                // 右子树不空
                stack.push(cur); // cur放回去暂不处理
                cur = cur.right; // 下一次循环处理cur的右子树,按左右根顺序后序遍历右子树
            }

        }
        return res;
    }

广度优先遍历BFS:

        层序遍历:将二叉树一层层的访问(一层层从上到下,从左到右访问二叉树)

        实现:借助队列实现,因为队列是先入先出,而层序遍历的结果也是从上到下从左到右搭建树的顺序,与队列类似。每当遍历一层节点结束,队列中恰好存储了下一层要遍历的节点。当整个队列为空,二叉树的层序遍历结束

 /**
     * 输入一颗树的根节点,返回存储层序遍历顺序的动态数组
     * @param root
     * @return
     */
    public List<E> BFSOrder(TreeNode<E> root){
        List<E> res = new ArrayList<>();
        Deque<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        TreeNode cur = null;
        while (!queue.isEmpty()){
            cur = queue.poll();
            res.add((E)cur.val); // Object下转型为E,E是包装类或自定义类对象
            if(cur.left!=null){
                queue.offer(cur.left);
            }
            if(cur.right!=null){
                queue.offer(cur.right);
            }
        }
        return res;
    }

1. 写一个方法统计当前二叉树中节点个数

int getNode(TreeNode root):当前二叉树中总共有几个节点

递归:仍然是先序遍历,只是此时的”访问“不是打印,而是统计节点个数

 /**
     * 传入一颗树的根节点,求出树的节点个数
     *
     * @param root
     * @return
     */
    public int countTreeNode(TreeNode<E> root) {
        if (root == null) {
            return 0;
        }
        // 当前树的节点总个数 = 根节点个数 + 左子树中节点总数 + 右子树中节点总数
        return 1 + countTreeNode(root.left) + countTreeNode(root.right);
    }

  

2. 统计一颗二叉树中叶子节点的个数

传入一颗以root为根的树,就能求出叶子节点的个数

 /**
     * 传入一颗树的根节点,求出叶子的节点个数
     *
     * @param root
     * @return
     */
    public int countLeafNode(TreeNode<E> root) {
        // 当前树为空
        if (root == null) {
            return 0;
        }
        // 找到叶子节点(即当前树只有一个节点)
        if (root.left == null && root.right == null) {
            return 1;
        }
        // 当前树的叶子节点总个数 = 左子树中节点总数 + 右子树中节点总数
        return countLeafNode(root.left) + countLeafNode(root.right);
    }

   

3. 求当前二叉树中第k层的节点个数

传入一颗以root为根的树,就能求出第k层的节点的个数,k<=树高

问题拆分:

以A为根节点求第三层的节点个数=以A的左树B为根节点求第二层节点个数+以A的右树C为根基的点求第二层节点个数

边界条件:

k<1 没有节点 return 0

k == 1 第一层,只有一个节点 return 1

 /**
     * 传入一颗树的根节点,求第k层节点的个数
     *
     * @param root
     * @param k
     * @return
     */
    public int countKthNode(TreeNode<E> root, int k) {
        // 当前树为空
        if (root == null) {
            return 0;
        }
        // 找到第k层
        if (k == 1) {
            return 1;
        }
        return countKthNode(root.left, k - 1) + countKthNode(root.right, k - 1);
    }

   

4. 求一颗二叉树的高度

传入一颗以root为根的树,就能求出树的高度

以A为根节点的树高 = 1+ max( 以B为根节点的树高x,以C为节点的树高)

/**
     * 传入一颗树的根节点,求该树的高度
     *
     * @param root
     * @return
     */
    public int countTreeDeep(TreeNode<E> root) {
        // 当前树为空
        if (root == null) {
            return 0;
        }
        // 当前树不为空
        // 当前树的高度 = 根节点高度 + 左子树高度和右子树高度中较大的
        return 1 + Math.max(countTreeDeep(root.left), countTreeDeep(root.right));
    }

   /**
     * 传入一颗树的根节点,判断一颗二叉树中是否包含指定的值val
     *
     * @param root
     * @param val
     * @return
     */
    public boolean findVal(TreeNode<E> root, E val) {
        // 当前树为空
        if (root == null) {
            return false;
        }
        // 当前树不空
        // 当前树是否包含val = 判断根节点的值是否为val 或 左子树的值是否为val 或 右子树的值是否为val
        return root.val == val || findVal(root.left, val) || findVal(root.right, val);
    }

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值