【数据结构-零基础学习】线索二叉树(代码+图示+解析)

【数据结构-零基础学习】线索二叉树(代码+图示+解析)

文章目录

  • 【数据结构-零基础学习】线索二叉树(代码+图示+解析)
    • @[toc]
      • 定义
      • 产生背景
      • 种类
      • 示意图
        • 1)未加入线索的普通二叉树示意图1.1
        • 2)线索添加的规则
        • 3)中序线索二叉树示意图1.2
        • 4)中序线索二叉树分析示意图1.3
      • 设计代码逻辑(重点)
      • 代码实现
          • ThreadedBinaryTree类
          • Test测试类
          • 测试结果
      • 优势

定义

​ 线索二叉树是一种二叉树的数据结构,它的特点在于空闲指针用于指向节点在某种特定遍历方式下的前驱或后继。在传统的二叉树中,每个节点有两个指针,指向其左孩子和右孩子。如果任一孩子不存在,相应的指针便为空。线索二叉树利用这些空指针,存储指向遍历序列中前驱或后继的指针,从而增加遍历效率。


产生背景

  • 线索二叉树产生的原因主要是为了提高二叉树的遍历效率。在未线索化的二叉树中,遍历元素(特别是非递归遍历)通常需要借助栈或者使用递归,这样会造成一定的空间和时间上的开销。线索二叉树通过利用空闲的指针字段,使得在遍历过程中能够直接找到节点的前驱和后继,从而避免了栈或递归的使用,减少了计算量和存储空间的需求。

  • 将非线性的二叉树结构转化成线性结构的一种方式是通过遍历,如果使用链式存储二叉树的时候,我们往往只能通过遍历的手段获得某个节点的前驱和后继,这是因为使用链式存储二叉树的时候会发现节点有两个指针域,一个是指向左孩子,一个指向右孩子.

  • 我们如何在非线性结构的二叉树中直接找到线性结构中的节点的前驱和后继节点的信息呢?

    • 我们现在可以获取的关于遍历序列的线性结构的二叉树的关于节点的前驱和后继节点的信息只有根节点和叶子节点,即 除根节点外每个节点都有且只有一个直接前驱节点,除叶子节点外的每个节点都有且只有一个后继节点,这是分析线性结构即遍历序列可以知道的信息)
  • 那么我们如何通过在非线性结构的二叉树中直接找到线性结构中所有节点的直接前驱节点和后继节点呢 ?

在这里插入图片描述


种类

线索二叉树中的“线索”是根据二叉树的遍历顺序添加的,所以前序、中序和后序线索二叉树在相同的二叉树结构上会有不同的线索指向。

  1. 中序线索二叉树:

​ 在这种类型的线索二叉树中,线索指向的是中序遍历的前驱和后继。也就是说,一个节点的左线索指向它的中序前驱,右线索指向它的中序后继。

  1. 前序线索二叉树:

在前序线索二叉树中,线索指向的是前序遍历的前驱和后继。因此,一个节点的左线索会指向它的前序前驱(如果存在),而右线索指向它的前序后继。

  1. 后序线索二叉树:

与前两者类似,后序线索二叉树中的线索指向的是后序遍历的前驱和后继。

  • 由于前序、中序、后序遍历的访问顺序不同,因此即使是相同的二叉树,在不同类型的线索二叉树中,节点的线索指向确实是不同的。这也意味着,为同一个二叉树构建前序、中序或后序线索二叉树时,需要进行不同的处理逻辑,以确保线索正确地指向正确的前驱或后继节点。

​ 我们在这里讲解的是 " 中序遍历二叉树的情况下给普通二叉树加入线索后构建中序线索二叉树 " !


示意图

1)未加入线索的普通二叉树示意图1.1

在这里插入图片描述

分析

上述的二叉树一共有n = 7 个节点,那么我们给这个二叉树的每个节点都添加了两个引用,那么我们可以知道 二叉树中引用一共有 2n= 14 个, 有n-1 = 6 个引用是被占用了,那么就还剩下 2n - (n-1) = n + 1 = 8 个空闲的引用.
那么我们如果把这空闲出来的当做线索,也就是我们刚刚改造普通二叉树变成线索二叉树的讲解的方法二,我们就可以知道把这 8 个引用利用起来,分别指向当前节点的直接前驱节点或直接后继节点,这样就完成了线索的添加.

而且我们还可以利用线性结构的二叉树遍历结果知道 :
除第一个节点无直接前驱节点已经最后一个节点无直接后继节点外其余节点一定都有直接前驱节点和直接后继节点.所以 8 个引用中会有两个引用是空的,方别是第一个节点和最后一个节点.

<所以现在我们可以中序遍历给每个节点都添加上对应的前驱和后继节点>


2)线索添加的规则
  1. 对于每个节点:
    • 如果节点的左指针为空,则将左指针指向其中序遍历的前驱节点,并标记左指针为线索。
    • 如果节点的右指针为空,则将右指针指向其中序遍历的后继节点,并标记右指针为线索。
  2. 遍历过程:
    • 从根节点开始,递归地对左子树进行中序遍历并线索化。
    • 访问当前节点,设置线索。
    • 递归地对右子树进行中序遍历并线索化。
3)中序线索二叉树示意图1.2

在这里插入图片描述

分析

的确,我们通过中序遍历这个二叉树的同时添加上线索之后,这个二叉树就叫做 线索二叉树


4)中序线索二叉树分析示意图1.3

在这里插入图片描述


设计代码逻辑(重点)

我在这里先提前设计一下实现代码的思路,这样可以理清代码的逻辑.

  • 代码的逻辑和实现框架概述:
  1. 类结构:
    • ThreadedBinaryTree: 主类,用于实现线索二叉树。
    • BinaryTreeNode: 嵌套静态类,表示二叉树的节点。
    • DataIllegalException: 自定义异常类,用于处理非法数据。
  2. 主要属性:
    • root: 二叉树的根节点。
    • pre: 用于在线索化过程中保存前一个节点。
    • size: 节点的数量。
  3. 构造方法:
    • 无参构造器初始化线索二叉树。
    • 带有根节点参数的构造器,用于创建带有根节点的线索二叉树。
  4. 核心方法:
    • insertNode(): 向线索二叉树中插入新节点。
    • inorderThreaded(): 对二叉树进行中序线索化。
    • inThreadList(): 中序遍历线索化后的二叉树。
  5. 辅助方法:
    • isEmpty(): 检查树是否为空。
    • size(): 返回树的大小。
    • getRoot(): 获取树的根节点。
    • getLeft(), getRight(): 分别获取节点的左孩子和右孩子。
    • checkParent(): 检查父节点是否为空。
    • checkRootDataIllegal(): 检查节点数据是否非法。

代码实现

ThreadedBinaryTree类
package src.test.java;

/**
 * 线索二叉树测试Threaded Binary Tree
 * 这个测试类目的是用来实现线索二叉树的(中序)
 */
public class ThreadedBinaryTree<E> {


    private BinaryTreeNode<E> root = null;//根节点
    private BinaryTreeNode<E> pre = null; // 用于保存前一个节点
    private int size;//节点个数

    /**
     *无参构造方法,可以完成部分属性的初始化
     */
    public ThreadedBinaryTree(){
    }

    /**
     *
     * @param root
     */
    public ThreadedBinaryTree(BinaryTreeNode<E> root){
        try {
            checkRootDataIllegal(root.val);
        } catch (DataIllegalException e) {
            throw new RuntimeException(e);
        }
        this.root = root;
        size++;
    }

    /**
     * 判空
     * @return 如果二叉树为空则返回ture
     */
    public boolean isEmpty(){
        return size==0;
    }

    /**
     * 检查数据是否为非法的数据
     * @param data val值
     * @throws DataIllegalException 自定义的异常(当数据为空的时候抛出该异常,并打印信息 "数据不能存储空值")
     */
    private void checkRootDataIllegal(E data) throws DataIllegalException {
        if (data==null){
        throw new DataIllegalException("数据不能存储空值");
        }
    }

    /**
     * 节点个数
     * @return 返回的二叉树节点的个数
     */
    public int size(){
        return size;
    }

    /**
     * 获取搜索二叉树的根节点
     * @return 根节点对象
     */
    public BinaryTreeNode<E> getRoot(){
        return this.root;
    }

    /**
     * 获取左孩子
     * @param parent 父亲节点
     * @return 返回的当前父亲节点的左孩子
     */
    public BinaryTreeNode<E> getLeft(BinaryTreeNode<E> parent){
        checkParent(parent);
        //注意需要判断传进来的父亲节点对象是否存在左孩子,如果存在才可以返回左孩子,如果不存在就返回null
        return parent.left;
    }

    /**
     * 获取右孩子
     * @param parent 父亲节点
     * @return 返回当前父亲节点的右孩子
     */
    public BinaryTreeNode<E> getRight(BinaryTreeNode<E> parent){
        //注意需要判断传进来的父亲节点是否存在右孩子,如果存在才可以返回右孩子对象,如果不存在就返回null
        return parent.right;
    }

    /**
     * 检查父亲节点是否为空,如果为空的话就需要抛出异常
     * @param parent 传进来的父亲节点
     */
    private void checkParent(BinaryTreeNode<E> parent){
        if(parent ==null){
            throw  new NullPointerException("父亲节点不能为空");
        }
    }


    /**
     * 向线索二叉树中插入节点
     * @param parent 父节点
     * @param child 要插入的新节点
     * @param isLeft 如果为true,则将节点插入为左孩子;如果为false,则为右孩子
     */
    public void insertNode(BinaryTreeNode<E> parent, BinaryTreeNode<E> child, boolean isLeft) {
        if (parent == null) {
            if (root == null) {
                root = child; // 如果根节点为空,则新节点成为根节点
            } else {
                throw new IllegalStateException("不能在空的父节点上插入");
            }
        } else {
            if (isLeft && parent.left == null) {
                parent.left = child;
            } else if (!isLeft && parent.right == null) {
                parent.right = child;
            } else {
                throw new IllegalStateException("指定的位置已有节点");
            }
        }
        size++;
    }


    // 线索遍历函数
    // 实现中序遍历的同时线索化
    public void inorderThreaded() {
        inorderThreaded(this.root);
    }

    private void inorderThreaded(BinaryTreeNode<E> node) {
        if (node == null) {
            return;
        }

        // 遍历左子树
        inorderThreaded(node.left);

        // 处理当前节点的前驱
        if (node.left == null) {
            node.left = pre;
            node.isLeftThread = true;
        }

        // 处理前一个节点的后继
        if (pre != null && pre.right == null) {
            pre.right = node;
            pre.isRightThread = true;
        }

        pre = node; // 更新前一个节点

        // 遍历右子树
        inorderThreaded(node.right);
    }


    /**
     * 中序遍历线索二叉树
     */
    public void inThreadList(BinaryTreeNode<E> root) {
        if (root == null) {
            return;
        }
        //查找中序遍历的起始节点
        while (root != null && !root.isLeftThread) {
            root = root.left;
        }

        while (root != null) {
            System.out.print(root.val + " ");
            // 如果右子节点是线索
            if (root.isRightThread) {
                root = root.right;
            } else {
                //有右子节点,遍历右子节点
                root = root.right;
                //如果右子节点不为null,并且右子节点的左子结点存在
                while (root != null && !root.isLeftThread) {
                    root = root.left;
                }
            }
        }

    }

    /**
     * 封装的节点类
     * @param <E>
     */
  static   class BinaryTreeNode<E> {
        //数据域
        public E val;
        //保存左孩子或者直接前驱节点的引用(保存直接前驱节点的条件是没有左孩子,如果有左孩子就直接指向左孩子)
        public BinaryTreeNode<E> left;
        //保存右孩子或者直接后继节点的引用(保存直接后继节点的条件是没有右孩子,如果有右孩子就直接指向右孩子)
        public BinaryTreeNode<E> right;

        public boolean isLeftThread;  // 左指针是否为线索
        public boolean isRightThread; // 右指针是否为线索

        /**
         * 构造方法
         *
         * @param val 创建节点的时候就初始化data的数值
         */
        public BinaryTreeNode(E val) {
            this.val = val;
            this.left = null;
            this.right = null;
            this.isLeftThread = false;
            this.isRightThread = false;
        }

        /**
         * 重写ToString()方法
         *
         * @return
         */
        public String toString() {
            return this.val.toString();
        }

    }

}

class DataIllegalException extends Exception{
    public DataIllegalException(String m){
        super(m);
    }
}
Test测试类
package src.test.java;

public class ThreadedBinaryTreeTest {
    public static void main(String[] args) {
        ThreadedBinaryTree<Integer> tree = new ThreadedBinaryTree<>();

        // 创建节点
        ThreadedBinaryTree.BinaryTreeNode<Integer> node6 = new ThreadedBinaryTree.BinaryTreeNode<>(6);
        //添加r根节点的左子结点node3
        ThreadedBinaryTree.BinaryTreeNode<Integer> node3 = new ThreadedBinaryTree.BinaryTreeNode<>(3);
        //添加r根节点的右子结点node7
        ThreadedBinaryTree.BinaryTreeNode<Integer> node7 = new ThreadedBinaryTree.BinaryTreeNode<>(7);
        //添加a节点的左子结点node1
        ThreadedBinaryTree.BinaryTreeNode<Integer> node1 = new ThreadedBinaryTree.BinaryTreeNode<>(1);
        //添加a节点的右子结点node4
        ThreadedBinaryTree.BinaryTreeNode<Integer> node4 = new ThreadedBinaryTree.BinaryTreeNode<>(4);
        //添加b节点的右子结点node2
        ThreadedBinaryTree.BinaryTreeNode<Integer> node2 = new ThreadedBinaryTree.BinaryTreeNode<>(2);
        //添加c节点的左子结点node5
        ThreadedBinaryTree.BinaryTreeNode<Integer> node5 = new ThreadedBinaryTree.BinaryTreeNode<>(5);




        // 插入节点
        tree.insertNode(null, node6, true);
        tree.insertNode(node6, node3,true);
        tree.insertNode(node6, node7, false);
        tree.insertNode(node3, node1, true);
        tree.insertNode(node3, node4, false);
        tree.insertNode(node1, null,true);
        tree.insertNode(node1, node2,false);
        tree.insertNode(node4, null, true);
        tree.insertNode(node4, node5, false);
        tree.insertNode(node2, null, false);
        tree.insertNode(node2, null, true);
        tree.insertNode(node5, null, true);
        tree.insertNode(node5, null, false);
        tree.insertNode(node7, null, true);

        // 线索化
        tree.inorderThreaded();

       //遍历二叉树
        tree.inThreadList(tree.getRoot());
    }
}
测试结果

在这里插入图片描述


优势

线索二叉树在特定的应用场景下提供了明显的优势,尤其是在空间利用率和遍历效率方面。然而,它并不适合所有场景,尤其是在频繁插入和删除操作的动态二叉树中,维护线索会带来额外的复杂性和开销。

  • 遍历效率提高

    线索二叉树允许无需递归或栈即可进行中序、前序或后序遍历,特别是中序遍历,它可以线性时间内完成,即O(n)。

  • 空间利用率提升

    传统的二叉树中大量空闲的指针空间得到了有效利用。

  • 遍历过程简化

    不需要维护复杂的栈结构或者进行递归调用,简化了遍历算法的实现。

  • 方便前驱和后继的查找

    对于某些特定的算法或数据结构,如线性化二叉树或转换为双向链表,线索二叉树提供了便捷的支持。


在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/175051.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2023年软件团队的六款最佳API文档工具

API开发的增长导致了大量的API文档工具的出现&#xff0c;这一点在使用谷歌搜索“API文档工具”时可以明显看到很多的搜索结果。这些工具的激增与全球API开发的扩张和对准确文档的需求增加相一致。值得关注的是&#xff0c;不仅小型创业公司进入了API市场&#xff0c;许多成熟企…

磐舟CI使用说明及案例

整体介绍 磐舟作为一个devops产品&#xff0c;它具备基础的CI流水线功能。同时磐舟的流水线是完全基于云原生架构设计的&#xff0c;在使用时会有一些注意事项。这里首先我们要了解磐舟整体的流水线打包逻辑。 文档结构说明 一般来说&#xff0c;磐舟推荐单个业务的标准git库…

外卖小程序系统:数字化餐饮的编码之道

在当今数字化时代&#xff0c;外卖小程序系统成为了餐饮业的一项技术巨制。这个系统不仅提供了便捷的点餐体验&#xff0c;更通过先进的技术手段&#xff0c;实现了高效订单处理、实时配送追踪以及个性化推荐。让我们深入了解外卖小程序系统的技术魔法&#xff0c;一起揭秘数字…

初识JVM(简单易懂),解开JVM神秘的面纱

目录 一、什么是JVM&#xff08;Java虚拟机&#xff09;&#xff1f; 二、JVM的功能 三、JVM的功能-即时编译 四、常见的JVM 五、JVM的组成 五、JVM的工作流程 参考资料 一、什么是JVM&#xff08;Java虚拟机&#xff09;&#xff1f; 在Java的世界里&#xff0c;Java虚…

维纳滤波器小结

维纳滤波器小结 一、问题概述 1.1 维纳滤波器简介 维纳滤波器是在最小均方误差&#xff08;mmse&#xff09;准则下的线性最优滤波器&#xff0c;其利用平稳随机过程的相关特性和频谱特性&#xff0c;对混有噪声的信号进行滤波。 其输入信号为 u ( n ) d ( n ) v ( n ) u…

适合您的iPhone手机的 8 款最佳手机数据恢复软件

当谈到恢复已删除或丢失的 iPhone 文件时&#xff0c;您通常有两种解决方案&#xff1a;从备份恢复、使用 iPhone 数据恢复软件。 虽然前者听起来很简单&#xff0c;但您可能已经检查过并且没有备份。那么您的下一个选择是尝试 iPhone 数据恢复工具。 市场上有许多软件工具都…

搭个网页应用,让ChatGPT帮我写SQL

大家好&#xff0c;我是凌览。 开门见山&#xff0c;我搭了一个网页应用名字叫sql-translate。访问链接挂在我的个人博客(https://linglan01.cn/about)导航栏&#xff0c;也可以访问https://www.linglan01.cn/c/sql-translate/直达sql-translate。 它的主要功能有&#xff1a;…

酷开科技OS——Coolita,让智能大屏走向国际

10月23日&#xff0c;2023中国—东盟视听传播论坛在南宁举行。作为第五届中国—东盟视听周重要活动之一&#xff0c;本次论坛以“共享新成果、共创新视听、共建新家园”为主题。来自中国和东盟的300余名专家学者、业界代表通过主旨演讲、主题发言、圆桌对话等方式进行深入探讨&…

Linux操作系统使用及C高级编程-D9D10Linux 服务搭建与使用

TFTP服务器 TFTP&#xff08;Trivial File Transfer Protocol&#xff09;即简单文件传输协议&#xff0c;是TCP/IP协议中一个用来在客户机与服务器之间进行简单文件传输的协议&#xff0c;提供不复杂、开销不大的文件传输服务。端口号为69 1、使用客户服务器方式和使用UDP数据…

STL中set的基本概念与使用

1 定义 1.1 set内元素唯一 1.2 set内元素默认升序排序 1.3 set内元素增&#xff0c;删&#xff0c;查时间复杂度都是O(logn) 2 使用 2.1 声明 set<int> mySet;2.2 插入元素 /*插入元素*/mySet.insert(5);mySet.insert(4);mySet.insert(3);mySet.insert(2);mySet.in…

Ajax基础(应用场景|jquery实现Ajax|注意事项|Ajax发送json数据|Ajax携带文件数据)

文章目录 一、Ajax简介二、基于jquery实现Ajax三、使用Ajax注意的问题1.Ajax不要与form表单同时提交2.后端响应格式问题3、使用了Ajax作为请求后的注意事项 四、前后端数据传输的编码格式(content-Type)1.urlencoded2.formdata3.application/json 五、Ajax携带文件数据六、Ajax…

基于SSM的网盘管理系统的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…

轻松上手Obsidian的图片操作 | Obsidian实践

前两天收到一位朋友留言&#xff0c;询问Obsidian笔记中图片的基本使用情况。 想到自己也好久没写文章了&#xff0c;便以此作为动力&#xff0c;基于自己有限经验&#xff0c;简单做个分享吧。 【问题1】图片是否可以通过截图粘贴板插入Obsidian笔记&#xff1f; 是可以实现的…

【C++】string类的介绍与使用

&#x1f9d1;‍&#x1f393;个人主页&#xff1a;简 料 &#x1f3c6;所属专栏&#xff1a;C &#x1f3c6;个人社区&#xff1a;越努力越幸运社区 &#x1f3c6;简 介&#xff1a;简料简料&#xff0c;简单有料~在校大学生一枚&#xff0c;专注C/C/GO的干货分…

并行与分布式 第7章 体系结构 下

文章目录 并行与分布式 第7章 体系结构 下7.3 互连结构7.3.1 网络拓扑的基本概念7.3.2 互连网络分类7.3.3 典型静态网络7.3.4典型动态互连网络 7.4 性能评测7.4.1 工作负载7.4.2 峰值速度7.4.3 并行执行时间7.4.4 性能价格比7.4.5多处理器性能定律 并行与分布式 第7章 体系结构…

普冉PY32系列(十一) 基于PY32F002A的6+1通道遥控小车II - 控制篇

目录 普冉PY32系列(一) PY32F0系列32位Cortex M0 MCU简介普冉PY32系列(二) Ubuntu GCC Toolchain和VSCode开发环境普冉PY32系列(三) PY32F002A资源实测 - 这个型号不简单普冉PY32系列(四) PY32F002A/003/030的时钟设置普冉PY32系列(五) 使用JLink RTT代替串口输出日志普冉PY32…

IvorySQL3.0:基于PG16.0最新内核,实现兼容Oracle数据库再升级

Oracle作为全球最大的数据库厂商之一&#xff0c;具有较高的市场知名度和份额。但随着数据处理需求日益增长&#xff0c;使用Oracle的企业可能面临一些挑战&#xff0c;如数据库复杂性、高昂维护成本、数据迁移和集成问题等&#xff0c;难以满足企业实时数据处理需求&#xff0…

构建 App 的方法

目录 构建 App 使用 App 设计工具以交互方式构建 App 使用 MATLAB 函数以编程方式构建 App 构建实时编辑器任务 可以使用 MATLAB 来构建可以集成到各种环境中的交互式用户界面。可以构建两种类型的用户界面&#xff1a; App - 基于用户交互执行操作的自包含界面 实时编辑器…

地奥集团大健康产业再添解酒黑科技:“酒必妥”!

地奥集团成都药业股份有限公司隶属于地奥集团旗下的子公司&#xff0c;至今已经超过百年历史&#xff0c;主要围绕化学药品在耕耘奉献。尽管公司历来都低调&#xff0c;但是地奥这块牌子在质量把控&#xff0c;安全生产把控等药品领域还是响当当。历年来&#xff0c;公司持续对…

穿越数据的迷宫-数据管理知识介绍

一、权威书籍介绍 《穿越数据的迷宫》 本书分12章重点阐述了数据管理的重要性&#xff0c;数据管理的挑战&#xff0c;DAMA的数据管理原则&#xff0c;数据伦理&#xff0c;数据治理&#xff0c;数据生命周期管理的规划和设计&#xff0c;数据赋能和数据维护&#xff0c;使用…