美文网首页codeER.tec
二叉树详细教程 --- 请食用

二叉树详细教程 --- 请食用

作者: 贪挽懒月 | 来源:发表于2020-11-07 16:24 被阅读0次

    为了后续学习堆排序以及MySQL索引等知识,接下来会重温一下树这种数据结构,包括二叉树、赫夫曼树、二叉排序树(BST)、平衡二叉树(AVL)、B树和B+树。

    一、树的介绍

    1. 为什么要有树这种结构?

    有了数组和链表,为什么还要有树?先来看看数组和链表的优缺点。

    • 数组:因为有索引,所以可以快速地访问到某个元素。但是如果要进行插入或者删除的话,被插入/删除位置之后的元素都得移动,如果插入后超过了数组容量,还得进行数组扩容。可见,数组查询快,增删慢。

    • 链表:没有索引,要查询某个元素,得从第一个元素开始,一个一个往后遍历。但是要进行插入或者删除,无需移动元素,只要找到插入/删除位置的前一个元素即可。所以链表查询慢,增删快。

    说到这里,那肯定知道树存在的意义了,没错,它吸收了链表和数组的优点,查询快,增删也快。


    欢迎大家关注我的公众号 javawebkf,目前正在慢慢地将简书文章搬到公众号,以后简书和公众号文章将同步更新,且简书上的付费文章在公众号上将免费。


    2. 二叉树:

    每个节点最多有两个叶子节点的树,叫做二叉树。假如一棵树有n层,所以的叶子节点都在第n层,并且节点总数为(2^n) - 1,那么就把这棵树称为满二叉树。如果最后一层的叶子节点左边是连续的,倒数第二层右边的叶子节点是连续的,那就称为完全二叉树

    二、二叉树的遍历

    前序遍历、后序遍历和中序遍历,这里的前中后只的是父节点的遍历时机。

    • 前序遍历:根左右。先输出当前节点;如果左子节点不为空,则递归进行前序遍历;如果右子节点不为空,则继续递归前序遍历。

    • 中序遍历:左根右。如果左子节点不为空,则递归中序遍历;输出当前节点;如果右子节点不为空,则递归中序遍历。

    • 后序遍历:左右根。如果左子节点不为空,递归后序遍历;如果右子节点不为空,递归后序遍历;输出当前节点。

    1. 新建一个TreeNode类:

    这个是节点类,省略了set/get方法。

    public class TreeNode {
        
        private Object element;
        private TreeNode left;
        private TreeNode right;
        
        public TreeNode() {}
        public TreeNode(Object element) {
            this.element = element;
        }
    }
    

    2. 构建一棵二叉树:

    二叉树

    假如要构建这样一棵树,那么代码实现就是:

    TreeNode root = new TreeNode(6);
    TreeNode node1 = new TreeNode(5);
    TreeNode node2 = new TreeNode(4);
    root.setLeft(node1);
    root.setRight(node2);
    TreeNode node3 = new TreeNode(3);
    node1.setLeft(node3);
    TreeNode node4 = new TreeNode(2);
    node1.setRight(node4);
    TreeNode node5 = new TreeNode(1);
    node2.setLeft(node5);
    

    按照遍历规则,前中后序的遍历结果应该是:

    • 前序遍历:6, 5, 3, 2, 4, 1
    • 中序遍历:3, 5, 2, 6, 1, 4
    • 后序遍历:3, 2, 5, 1, 4, 6

    3. 代码实现三种遍历方式:

    /**
     * 前序遍历
     * 
     * @param root
     */
    public static void preOrder(TreeNode root) {
        // 先输出当前节点
        System.out.println(root.getElement());
        // 判断左子节点是否为空,不为空就递归
        if (root.getLeft() != null) {
            preOrder(root.getLeft());
        }
        // 判断右子节点是否为空,不为空就递归
        if (root.getRight() != null) {
            preOrder(root.getRight());
        }
    }
    
    /**
     * 中序遍历
     * 
     * @param root
     */
    public static void infixOrder(TreeNode root) {
        // 判断左子节点是否为空,不为空就递归
        if (root.getLeft() != null) {
            infixOrder(root.getLeft());
        }
        // 输出当前节点
        System.out.println(root.getElement());
        // 判断右子节点是否为空,不为空就递归
        if (root.getRight() != null) {
            infixOrder(root.getRight());
        }
    }
    
    /**
     * 后序遍历
     * @param root
     */
    public static void postOrder(TreeNode root) {
        // 判断左子节点是否为空,不为空就递归
        if (root.getLeft() != null) {
            postOrder(root.getLeft());
        }
        // 判断右子节点是否为空,不为空就递归
        if (root.getRight() != null) {
            postOrder(root.getRight());
        }
        // 输出当前节点
        System.out.println(root.getElement());
    }
    

    二叉树的查找就不说了,都会遍历了还不会查找吗?

    三、二叉树的删除

    这里说的删除先不考虑子节点上浮的情况,即如果删除的非叶子节点,那就直接删除整棵子树。删除的思路如下:

    • 如果二叉树只有一个节点,直接将该节点设置为null即可;
    • 判断当前节点的左子节点是否为要删除的节点,如果是,就删除当前节点左子节点;
    • 判断当前节点的右子节点是否为要删除的节点,如果是,就删除当前节点右子节点;
    • 如果上述操作没有找到要删除的节点,就向当前节点左子树递归;
    • 如果向左子树递归也没找到要删除的节点,就向当前节点右子树递归;

    代码实现:

    /**
    * 删除节点
    * @param node
    */
    public static void delNode(TreeNode root, Object ele) {
        // 如果二叉树为空,直接return
        if (root == null) {
            return;
        }
        // 如果只有一个节点,或者root就是要删除的节点,直接置空
        if ((root.getLeft() == null && root.getRight() == null) ||
                root.getElement() == ele) {
            root.setElement(null);
            root.setLeft(null);
            root.setRight(null);
            return;
        }
        // 判断左子节点是否为要删除的节点
        if (root.getLeft() != null && root.getLeft().getElement()== ele) {
            root.setLeft(null);
            return;
        }
        // 判断右子节点是否为要删除的节点
        if (root.getRight() != null && root.getRight().getElement()== ele) {
            root.setRight(null);
            return;
        }
        // 向左子树递归
        if (root.getLeft() != null) {
            delNode(root.getLeft(), ele);
        }
        // 向右子树递归
        if (root.getRight() != null) {
            delNode(root.getRight(), ele);
        }
    }
    

    四、顺序存储二叉树

    所谓顺序存储二叉树,就是将二叉树的元素用数组存起来,并且在数组中遍历这些元素时依旧能体现出前/中/后序遍历。为了达到这个目的,所以顺序存储二叉树有一些要求:

    • 通常只考虑完全二叉树;

    我们给二叉树的元素从上到下从左往右从0开始依次标上号,这些号得满足:

    • n号元素的左节点标号为2n + 1
    • n号元素的右节点标号为2n + 2
    • n号元素的父节点标号为(n-1) / 2

    怎么将二叉树用数组存起来就不说了,进行层序遍历就好了,从上到下从左往右将元素依次存进数组。主要来看一看,用数组保存起来的二叉树。如何遍历,才能达到二叉树前/中/后序遍历的效果。

    代码实现:

    /**
    * 前序遍历顺序存储的二叉树
    * @param arr
    */
    public static void preOder(int[] arr) {
        if (arr == null || arr.length == 0) {
            return;
        }
        preOder(arr, 0);
    }
        
    /**
    * 前序遍历顺序存储的二叉树
    * @param index
    */
    private static void preOder(int[] arr, int index) {
        // 输出当前元素
        System.out.println(arr[index]);
        // 向左递归
        if ((index * 2 + 1) < arr.length) {
            preOder(arr, (index * 2 + 1));
        }
        // 向右递归
        if ((index * 2 + 2) < arr.length) {
            preOder(arr, (index * 2 + 2));
        }
    }
    

    这就是实现前序遍历的代码,中/后序遍历就换一下输出当前元素的位置就可以了。

    五、线索化二叉树

    二叉树

    还是拿这棵二叉树来说,321节点的leftright指针都没用到,4节点的right指针没有用到,也就是整棵二叉树有7个指针是没有用到的。其实我们可以充分利用这些指针,让这些指针指向前/中/后序遍历的前/后一个节点,这就叫线索化二叉树。根据指针指向的不同节点,又可以分为前/中/后序线索化二叉树。

    注意,要线索化二叉树,得满足一个条件,假如总共有n个节点,那么未使用的指针数应为n + 1。一个节点的前一个节点称为前驱节点,后一个节点称为后继节点。

    1. 中序线索化二叉树:

    上面这棵二叉树,中序遍历的结果是:3, 5, 2, 6, 1, 4,我们让有空闲指针的节点,left指针指向它的前驱,right指针指向它的后继。首先从3开始,3没有前驱,后继是5,所以3right指针指向5;然后是2,让它left指向5right指向61left指向6right指向4。中序线索化的二叉树如下图(绿色是左指针,黄色是右指针):

    中序线索化二叉树
    • 线索化二叉树后,left指针可能指向的是左子树,也可能指向前驱节点;
    • right指针可能指向右子树,也可能指向后继节点;

    2. 代码实现:

    首先,对于TreeNode节点类,得增加两个属性,用来表示左右节点的类型,约定用0表示子树,用1表示前驱/后继。改造后的节点类如下:

    public class TreeNode {
        private Object element;
        private TreeNode left;
        private TreeNode right;
        private int leftType; // 0左子树, 1前驱节点
        private int rightType; // 0右子树,1后继节点
    }
    

    上面所有操作二叉树的方法,我都封装在TreeUtil中,要线索化二叉树,还需要在TreeUtil中定义一个变量,用来保存前一个节点,如下:

    private static TreeNode preNode; // 前一个节点
    

    线索化二叉树的代码:

    public static void inSeqLineTree(TreeNode curNode) {
            if (curNode == null){
                return;
            }
            // 处理左子树
            inSeqLineTree(curNode.getLeft());
    
            // 处理当前节点
            if (curNode.getLeft() == null){
                curNode.setLeft(preNode);
                curNode.setLeftType(1);
            }
            if (preNode != null && preNode.getRight() == null){
                preNode.setRight(curNode);
                preNode.setRightType(1);
            }
            preNode = curNode;
    
            // 处理右子树
            inSeqLineTree(curNode.getRight());
    }
    

    那么怎么验证有没有线索化成功呢?如果成功的话,节点3right应该是节点5,并且节点3的型是1;节点2left5,并且节点2的类型是1。

    public static void main(String[] args) {
            TreeNode root = new TreeNode(6);
            TreeNode node1 = new TreeNode(5);
            TreeNode node2 = new TreeNode(4);
            root.setLeft(node1);
            root.setRight(node2);
            TreeNode node3 = new TreeNode(3);
            node1.setLeft(node3);
            TreeNode node4 = new TreeNode(2);
            node1.setRight(node4);
            TreeNode node5 = new TreeNode(1);
            node2.setLeft(node5);
    
            TreeUtil.inSeqLineTree(root); // 6, 5, 3, 2, 4, 1
    
            System.out.printf("节点3的right:%s, 类型:%s %n", node3.getRight().getElement(), node3.getRightType());
            System.out.printf("节点2的left:%s, 类型:%s %n", node4.getLeft().getElement(), node4.getLeftType());
    }
    
    运行结果

    结果和预期一致,说明线索化成功。

    六、遍历线索化二叉树

    上面线索化了二叉树,有什么用呢?其实就是为了可以更简单地进行遍历。线索化之后,各个节点的指向有变化,所以原来的遍历方式就用不了了,现在可以用非递归的方式进行遍历,可以提高效率。

    遍历线索化二叉树的代码:

    public static void seqOrder(TreeNode root) {
            TreeNode curNode = root;
            while(curNode != null) {
                // 找到leftType为1的节点
                while(curNode.getLeftType() == 0) {
                    curNode = curNode.getLeft();
                }
                // 找到就输出
                System.out.println(curNode.getElement());
                // 如果当前节点的右指针指向的是后继节点,就直接输出
                while(curNode.getRightType() == 1) {
                    curNode = curNode.getRight();
                    System.out.println(curNode.getElement());
                }
                // 遇到了不等于1的,替换遍历的节点
                curNode = curNode.getRight();
            }
    }
    

    传入root后,运行结果是和中序遍历的结果一致的,说明没问题。

    相关文章

      网友评论

        本文标题:二叉树详细教程 --- 请食用

        本文链接:https://www.haomeiwen.com/subject/xncfmktx.html