当前位置:

首页 > 编程开发 > 如何使用Java实现线索化二叉树数据结构?

如何使用Java实现线索化二叉树数据结构?

1.线索化二叉树的介绍将数列{1,3,6,8,10,14}构建成一颗二叉树.问题分析:1.当我们对上面的二叉树进行中序遍历时,数列为{8,3,10,1,6,14}2.但是6,8,10,14这几个节点的左右指针,并没有完全的利用上.3.如果我们希望充分的利用各个节点的左右指针,让各个节点可以指向自己的前后节点,怎么办?4.解决方案-线索二叉树概念:当用二叉链表作为二叉树的存储结构时,可以很方便的找到某个结点的左右孩子;但一般情况下,无法直接找到该结点在某种遍历序列中的前驱和后继结点。所以使用线索化,利用二叉

本文目录

    1.线索化二叉树的介绍

    将数列 {1, 3, 6, 8, 10, 14 } 构建成一颗二叉树.

    如何使用Java实现线索化二叉树数据结构?

    问题分析:

    1.当我们对上面的二叉树进行中序遍历时,数列为 {8, 3, 10, 1, 6, 14 }

    2.但是 6, 8, 10, 14 这几个节点的 左右指针,并没有完全的利用上.

    3.如果我们希望充分的利用 各个节点的左右指针, 让各个节点可以指向自己的前后节点,怎么办?

    4.解决方案-线索二叉树

    概念:当用二叉链表作为二叉树的存储结构时,可以很方便的找到某个结点的左右孩子;但一般情况下,无法直接找到该结点在某种遍历序列中的前驱和后继结点。所以使用线索化,利用二叉树结构链表的空指针域进行线索化。

    2.线索化二叉树的基本特点

    n 个结点的二叉链表中含有 n+1 【公式 2n-(n-1)=n+1】 个空指针域。利用二叉链表中的空指针域,存放指向该结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索")

    这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种

    3.线索化二叉树的应用案例

    中序线索化二叉树并遍历

    应用案例说明:将下面的二叉树,进行中序线索二叉树。中序遍历的数列为 {8, 3, 10, 1, 14, 6}

    如何使用Java实现线索化二叉树数据结构?

    思路分析

    中序遍历的结果:{8, 3, 10, 1, 14, 6}

    如何使用Java实现线索化二叉树数据结构?

    那么线索化之后的二叉树的左右指针如上图↑

    说明: 当线索化二叉树后,Node 节点的 属性 left 和 right ,有如下情况:

    • left 指向的是左子树,也可能是指向的前驱节点. 比如 ① 节点 left 指向的左子树, 而 ⑩ 节点的 left 指向的 就是前驱节点.

    • right 指向的是右子树,也可能是指向后继节点,比如 ① 节点 right 指向的是右子树,而⑩ 节点的 right 指向的是后继节点.

    因此要改变原来的二叉树的结点结构

    package com.studySelf.tree.threadedBinaryTree;
    
    /**
     * @author wang
     * @version 1.0
     * @packageName com.studySelf.tree.tree01
     * @className Node
     * @date 2022/4/19 20:15
     * @Description Node结点
     */
    public class Node {
        //唯一编号
        private int num;
        //结点的值
        private String name;
        //左结点
        private Node left;
        //右节点
        private Node right;
    
        //这个属性用来控制线索二叉树的结点的指向,0代表指向左结点,1代表指向前趋结点
        private int leftType;
        //这个属性用来控制线索二叉树的结点的指向,0代表指向右结点,1代表指向后继结点
        private int rightType;
    
    
        //构造方法
    
        public Node(int num, String name) {
            this.num = num;
            this.name = name;
        }
    
        public int getLeftType() {
            return leftType;
        }
    
        public void setLeftType(int leftType) {
            this.leftType = leftType;
        }
    
        public int getRightType() {
            return rightType;
        }
    
        public void setRightType(int rightType) {
            this.rightType = rightType;
        }
    
        public int getNum() {
            return num;
        }
    
        public void setNum(int num) {
            this.num = num;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public Node getLeft() {
            return left;
        }
    
        public void setLeft(Node left) {
            this.left = left;
        }
    
        public Node getRight() {
            return right;
        }
    
        public void setRight(Node right) {
            this.right = right;
        }
    
        @Override
        public String toString() {
            return "Node{" +
                    "num=" + num +
                    ", name='" + name +
                    '}';
        }
    
        /**
         * 前序遍历
         */
        public void preSelect() {
            //首先输出根结点
            System.out.println(this);
            //其次判断是否有左结点
            if (this.left != null) {
                //没有左结点,就递归调用本方法输出该结点。
                this.left.preSelect();
            }
            if (this.right != null) {
                this.right.preSelect();
            }
        }
    
        /**
         * 中序遍历
         */
        public void infixSelect() {
            //首先判断左结点
            if (this.left != null) {
                //如果左结点不为空,递归向左子树调用
                this.left.infixSelect();
            }
            //当左结点为空,再输出根结点。当他本身就是最后一个左结点的时候,会直接输出,且没有右节点
            System.out.println(this);
            if (this.right != null) {
                //右节点同样如此,递归调用。直到没有结点为止。
                this.right.infixSelect();
            }
        }
    
        /**
         * 设二叉树有三个结点,根结点,左结点,右节点。
         * 后序遍历,解释,当一个二叉树的左结点不为空,那么他会进入下一个递归调用自己的后序遍历方法
         * 此时,根结点就是左结点,这时判断左结点,右节点均为空,就会输出左结点
         * 回退到根结点为this的时候,左结点已经判断完毕,接下来是右节点,右节点不为空,进入后续遍历递归,
         * 此时的this就是右节点,进入递归后,判断,不存在左右结点,输出this,也就是整个二叉树的右节点
         * 回退到this为根结点时,右节点也已经输出,走到最后一步,输出自己也就是this。
         * 整个后序遍历就结束,那么该二叉树的遍历结果就是左,右,根
         */
    
        public void afterSelect() {
            if (this.left != null) {
                this.left.afterSelect();
            }
    
            if (this.right != null) {
                this.right.afterSelect();
            }
            System.out.println(this);
        }
    
        /**
         * @param num
         * @Date 2022/4/21 17:51
         * @Param
         * @Return Node
         * @MetodName preSearchByNum
         * @Author wang
         * @Description 根据结点的编号来查询结点, 前序遍历查询,根,左,右
         */
        public Node preSearchByNum(int num) {
            //首先判断传进来的num与该结点的num是否相等
            //如果相等,那该结点就是我们要找的结点。
            if (this.num == num) {
                return this;
            }
    
            //如果不相等,该结点就不是我们要找的结点
            //那么我们就遍历该结点的左子节点,和右子结点
            //定义一个结点用于接收最后的返回结果
            Node resultNode = null;
            //如果该结点的左子结点不为空,就递归调用前序遍历,继续查找,如果找到了,那么resultNode就是我们想要的值
            if (this.left != null) {
                resultNode = this.left.preSearchByNum(num);
            }
    
            //如果遍历完左子结点,已经找到了我们想要的结果,直接返回结果即可,
            if (resultNode != null) {
                return resultNode;
            }
    
            //如果左子结点为空,且没有找到我们想要的结点的情况下。那就判断右子结点
            if (this.right != null) {
                resultNode = this.right.preSearchByNum(num);
            }
            //最后,如果找到了,那么resultNode一定会被赋值,如果没找到,就会返回null
            return resultNode;
        }
    
        /**
         * @param num
         * @Date 2022/4/21 17:58
         * @Param
         * @Return Node
         * @MetodName infixSearchByNum
         * @Author wang
         * @Description 中序遍历查找,左,根,右进行查询即可。
         */
        public Node infixSearchByNum(int num) {
            //首先判断左子结点,如果存在左子结点,递归继续查询遍历即可即可。
            Node resultNode = null;
            if (this.left != null) {
                resultNode = this.left.infixSearchByNum(num);
            }
    
            //如果左子结点已经找到了我们想要的结点,直接返回当前结点即可
            if (resultNode != null) {
                return resultNode;
            }
    
            //判断根结点
            if (this.num == num) {
                return this;
            }
    
            //判断右子结点,
            if (this.right != null) {
                resultNode = this.right.infixSearchByNum(num);
            }
            //最后返回我们的结果即可。
            return resultNode;
        }
    
    
        /**
         * @param num
         * @Date 2022/4/21 19:15
         * @Param
         * @Return Node
         * @MetodName afterSearchNum
         * @Author wang
         * @Description 后续遍历结点进行查找结点。左,右,根
         */
        public Node afterSearchNum(int num) {
            Node resultNode = null;
            //首先遍历左结点
            if (this.left != null) {
                resultNode = this.left.afterSearchNum(num);
            }
    
            //判断左结点是否找到哦啊
            if (resultNode != null) {
                return resultNode;
            }
    
            //判断右节点是否为空
            if (this.right != null) {
                resultNode = this.right.afterSearchNum(num);
            }
    
            //判断右节点是否找到
            if (resultNode != null) {
                return resultNode;
            }
    
            //判断根结点是否为我们找的结点
            if (this.num == num) {
                return this;
            }
            //最后返回
            return resultNode;
        }
    
        /**
         * @param num
         * @Date 2022/4/25 19:30
         * @Param
         * @Return void
         * @MetodName delNodeByNum
         * @Author wang
         * @Description 根据结点的编号删除结点
         */
        public void delNodeByNum(int num) {
            //首先,判断当前结点的左结点是否为空,并且左结点的num是否与num相等
            if (this.left != null && this.left.num == num) {
                //如果相等,那就说明该结点就是我们要删除的结点,将其左结点置空即可
                this.left = null;
                return;
            }
    
            //如果左结点没有执行,说明左结点没有我们想要的结果,也就是要删除的结点不在左结点
            //那么就对右节点进行判断
            if (this.right != null && this.right.num == num) {
                this.right = null;
                return;
            }
    
            //如果左右结点均没有找到目标结点
            //那么就对左子树进行递归删除操作
            if (this.left != null) {
                this.left.delNodeByNum(num);
            }
    
            //同理,如果左子树没有目标结点,向右子树进行递归删除操作
            if (this.right != null) {
                this.right.delNodeByNum(num);
            }
    
        }
    }

    可以看到我们多出来了这么两个属性:

        //这个属性用来控制线索二叉树的结点的指向,0代表指向左结点,1代表指向前趋结点
        private int leftType;
        //这个属性用来控制线索二叉树的结点的指向,0代表指向右结点,1代表指向后继结点
        private int rightType;

    中序线索化二叉树

      /**中序线索化二叉树*/
        /**
         * @param node 该结点为根结点,从根节点开始线索化二叉树,中序
         */
        public void infixThreadNodes(Node node) {
            /**首先判断二叉树的根节点上会否为空,如果根结点为空,不可以线索化,因为没有二叉树*/
            if (node == null) {
                return;
            }
    
            /**接着对左子树进行线索化*/
            infixThreadNodes(node.getLeft());
    
            /**对当前结点进行线索化*/
            /**首先判断当前结点的左子结点是否为空*/
            if (node.getLeft() == null) {
                //如果左子结点为空,说明就找到了我们需要线索化的结点
                //就可以将pre也就是一直在当前结点的前趋结点设置给当前结点的左子结点,
                //如果这是第一个结点,那么pre为空,正好第一个结点的前趋结点也为空
                node.setLeft(pre);
                //并且将它的左子结点的类型设置为前趋结点。1代表前趋结点
                node.setLeftType(1);
            }
    
            /**接着判断前趋结点的右子结点是否为空,前提是前趋结点不能为空,如果他为空,说明这是第一个结点还没走完*/
            if (pre != null && pre.getRight() == null) {
                //如果条件满足,说明前趋结点现在已经走到了值,并且还没有线索到后继结点,
                // 也就是当前结点的上一个结点(这个上一个结点就是当前的前趋结点)还没有后继,
                //那么上一个结点的后继结点就是当前结点,因此赋值前趋结点(也就是上一个结点)的后继结点为当前结点。
                //这样这条线索就连接上了,并且只有满足叶子结点的结点才可以进行线索化
                pre.setRight(node);
                pre.setRightType(1);
            }
    
            //当前两步走完之后,就可以将pre结点赋值为当前结点,
            // 因为下一个结点一走,当前结点就是前趋结点了。直到走到全部线索化结束
            //这步十分重要,这一步不写,整个线索化就连接不上
            pre = node;
    
            /**对右子树进行线索化*/
            infixThreadNodes(node.getRight());
        }

    中序线索化二叉树思路

    1. 因为中序遍历的二叉树顺序是左根右,因此,首先对左子树进行线索化,递归线索化即可;

    2. 当递归到左子树的最左结点的时候,他的左结点肯定为空,那么就对他的左结点赋值为pre(pre结点是在线索化的最后一步赋值为当前结点,这样递归才能进行下去),注意左结点的类型一定要改为1,代表他是前趋结点。前趋结点就线索掉了。

    3. 后继结点的处理则是判断前趋结点,当前趋结点不为空,并且前趋结点的右节点为空,那么设置前趋结点的右节点为当前结点,也就是上一个结点未设置的右节点,类型同样要设置为后继

    4. 最后就是对pre这个结点赋值,为当前结点,因为下一次递归,当前结点就成了上一个结点,也就是这里的pre

    5. 最后就是对二叉树的右子结点进行线索化。

    中序线索化二叉树的遍历

    1. 遍历中序线索化二叉树首先应该明确的是他的遍历顺序要和遍历原来的中序二叉树遍历的结果是一样的,才是遍历成功

    2. 那么第一步应该就是判断根结点不为空,也就是循环结束条件

    3. 接着就循环查找当前结点的左子结点类型为0的也就是没有被线索化的结点,只要他为0,那么结点就一直往左子结点赋值。直到找到第一个被线索化的结点,输出他,他就是我们第一个线索化并且也是中序遍历的第一个左子结点。

    4. 输出之后,判断他的右子结点是否被线索化,如果被线索化,那么当前结点node就被赋值为它自己的右子结点,并且输出,如果他之后的结点的右子结点的类型又为1,那么继续往后走并赋值,说明他有后继

    5. 直到右子结点的类型为0,退出循环之后,也应该向右再赋值,继续向后遍历

    代码演示

        /**遍历中序线索化二叉树*/
        public void infixThreadNodesSelect() {
            /**第一个结点为根结点*/
            Node node = root;
            while(node != null) {
                /**当结点不为空,就一直遍历*/
                /**当该结点的左子结点的类型为1的时候,也就是说该结点是被线索化的结点,
                 * 因为是中序遍历,所以应该遍历到最左边的最左子结点,那么就是第一个
                 * 左子结点类型为1的结点。*/
                while(node.getLeftType() == 0) {
                    node = node.getLeft();
                }
                /**当左子结点的类型为1,输出左子结点*/
                System.out.println(node);
    
                /**当右子结点类型为1,当前结点输出完毕后
                 * 中序遍历就应该输出右子结点,那么就是当前结点的右子结点类型只要为1,就往后移动
                 * 并且输出,当他为0,说明没有线索化,那么没有后继结点,但是他有右子结点,
                 * 因此要在循环结束以后向后移动。*/
                while (node.getRightType() == 1) {
                    node = node.getRight();
                    System.out.println(node);
                }
                /**当右子结点循环退出,说明这里到了类型为0也就是后继结点*/
                node = node.getRight();
            }

    4.前序线索化二叉树、遍历

    线索化二叉树

     /**
         * 前序线索化二叉树
         */
        public void preThreadNodes(Node node) {
            /**首先判断当前节点是否为空*/
            if (node == null) {
                return;
            }
    
            /**如果是前序遍历,那么先对当前结点进行判断,当前结点的前趋结点的操作*/
            if (node.getLeft() == null) {
                node.setLeft(pre);
                node.setLeftType(1);
            }
    
            /**处理后继结点,定义的前趋结点不为空,说明他有值,就是已经存在了上一个结点的值,他的右子结点没有值的话,就可以
             * 给他赋予后继结点为当前结点,这里赋予的也就是上一个结点*/
            if (pre != null && pre.getRight() == null) {
                pre.setRight(node);
                pre.setRightType(1);
            }
    
            /**这里是关键的一步*/
            pre = node;
    
            /**对左子结点进行线索化,注意,这里需要排除已经被线索化掉的结点,因为这里要考虑一个情况,
             * 比如这里已将到了最下面一个左子结点,由于是前序遍历,遍历到左子结点,他的前趋结点在上面就设置了
             * 如果这里不判断左子结点的类型,那么就会进入递归,但是这个递归如果进去了,就是错误的递归,因为他传过去的结点
             * 是我们刚刚给他赋值的前趋结点,也就是根结点。会发生错误。因此得判断类型*/
            if (node.getLeftType() != 1) {
                preThreadNodes(node.getLeft());
            }
    
    
            /**对右子结点进行递归线索化*/
            if (node.getRightType() != 1) {
                preThreadNodes(node.getRight());
            }
        }

     遍历线索化二叉树

    /**
         * 前序遍历线索二叉树*/
        public void preThreadNodeSelect() {
            Node node = root;
            while(node !=null) {
                while(node.getLeftType() == 0) {
                    /**前序遍历为根左右,先输出根结点,因为他每次进来循环肯定是先到根结点,所以一进该循环
                     * 就要输出根结点,当他的lefttype=1循环结束,说明遇到了被线索化的地方了。*/
                    System.out.println(node);
                    /**再最左子结点进行遍历*/
                    node = node.getLeft();
                }
                /**上面的循环结束之后就应该输出当前结点,也就是那个序列化的结点
                 * 之后结点向右移动继续遍历*/
                System.out.println(node);
                node = node.getRight();
            }
      }

    图解

    如何使用Java实现线索化二叉树数据结构?

    5.后序线索化二叉树

    后续线索化二叉树

    /**
     * 后序线索化二叉树的方法
     */
    public void postThreadedBinaryTree(Node node) {
        /**首先判断结店不能为空*/
        if (node == null) {
            return;
        }
    
        /**先后续线索化左子结点*/
        postThreadedBinaryTree(node.getLeft());
        /**在后续线索化右子结点*/
        postThreadedBinaryTree(node.getRight());
    
        /**处理当前结点的前趋结点*/
        if (node.getLeft() == null) {
            node.setLeft(pre);
            node.setLeftType(1);
        }
    
        /**处理后继结点*/
        if (pre != null && pre.getRight() == null) {
            pre.setRight(node);
            pre.setRightType(1);
        }
        pre = node;
    }
    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发 Java
    相关文章 更多
    Java单元测试入门:使用JUnit 5编写第一个测试用例
    Java单元测试入门:使用JUnit 5编写第一个测试用例

    面向初学者的JUnit 5实用入门指南。从测试概念、Maven依赖配置,到手把手编写首个单元测试用例及断言验证,助你掌握Java单元测试的核心流程。

    Java变量未初始化报错解析:局部变量与成员变量默认值差异
    Java变量未初始化报错解析:局部变量与成员变量默认值差异

    深入解析Java中局部变量未初始化编译报错与成员变量自动赋予默认值的机制差异,涵盖JVM内存分配、编译期确定性赋值检查与实际排查修复方案。

    Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat
    Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat

    详细讲解在Java中获取当前系统时间并进行格式化输出的两种核心方案:基于Java 8全新日期时间API的DateTimeFormatter与传统遗留的SimpleDateFormat,包含完整代码示例、线程安全机制对比及生产选型建议。

    Java中final与static final常量的定义及初始化规则
    Java中final与static final常量的定义及初始化规则

    深入解析Java中final变量与static final常量的定义、内存语义、初始化时机及合法赋值路径,涵盖编译期常量优化与实战避坑指南。

    Java抽象方法与接口方法的区别及选型建议
    Java抽象方法与接口方法的区别及选型建议

    深入对比Java中抽象类抽象方法与接口方法的语法演进、设计意图及运行期差异,并提供清晰的架构选型决策流程与实战范例。

    Java实现HTTP请求与JSON解析的完整代码示例
    Java实现HTTP请求与JSON解析的完整代码示例

    本文提供Java标准库HttpClient发起HTTP GET/POST请求并结合Jackson进行JSON数据反序列化与序列化的完整代码示例与工程实践指南。

    Java递归出现StackOverflowError的原因分析及栈空间优化方案
    Java递归出现StackOverflowError的原因分析及栈空间优化方案

    详细分析Java递归调用出现StackOverflowError的原因,结合代码说明线程栈、栈帧和递归深度之间的关系,并介绍终止条件排查、迭代改写、减少栈帧开销以及通过-Xss调整JVM栈空间的方法。

    Java空指针异常(NPE)的代码排查与修复实例
    Java空指针异常(NPE)的代码排查与修复实例

    针对Java中常见的NullPointerException(NPE),本文从堆栈日志定位、典型代码触发场景、具体修复代码对比到防御体系进行系统讲解,提供实用且易上手的排查与修复方案。

    selenium上传文件教程本地文件路径设置方法
    selenium上传文件教程本地文件路径设置方法

    深入解析Selenium处理文件上传的技术细节,涵盖标准HTML控件的路径直接注入法与非标准控件的系统级模拟方案,提供Windows与Linux环境下的路径设置最佳实践及常见异常排查。

    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景
    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景

    针对大数据工程场景,系统对比Java、Scala与Python在执行性能、生态结合、开发效率和维护成本上的核心差异,给出清晰的架构选型依据与落地方案。

    查看更多
    精品专题 更多
    装机必备
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows
    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程
    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    Mac软件 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    Aperty macOS版
    Aperty macOS版
    macOS

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    WINDOWS 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    PixTV 网页版
    PixTV 网页版
    Windows

    PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。