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

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 【数据结构-零基础学习】线索二叉树(代码+图示+解析)

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


定义

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


产生背景

  • 线索二叉树产生的原因主要是为了提高二叉树的遍历效率。在未线索化的二叉树中,遍历元素(特别是非递归遍历)通常需要借助栈或者使用递归,这样会造成一定的空间和时间上的开销。线索二叉树通过利用空闲的指针字段,使得在遍历过程中能够直接找到节点的前驱和后继,从而避免了栈或递归的使用,减少了计算量和存储空间的需求。
  • 将非线性的二叉树结构转化成线性结构的一种方式是通过遍历,如果使用链式存储二叉树的时候,我们往往只能通过遍历的手段获得某个节点的前驱和后继,这是因为使用链式存储二叉树的时候会发现节点有两个指针域,一个是指向左孩子,一个指向右孩子.
  • 我们如何在非线性结构的二叉树中直接找到线性结构中的节点的前驱和后继节点的信息呢?
  • 我们现在可以获取的关于遍历序列的线性结构的二叉树的关于节点的前驱和后继节点的信息只有根节点和叶子节点,即 除根节点外每个节点都有且只有一个直接前驱节点,除叶子节点外的每个节点都有且只有一个后继节点,这是分析线性结构即遍历序列可以知道的信息)
  • 那么我们如何通过在非线性结构的二叉树中直接找到线性结构中所有节点的直接前驱节点和后继节点呢 ?


种类

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

  1. 中序线索二叉树:

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

  1. 前序线索二叉树:

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

  1. 后序线索二叉树:

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

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

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


示意图

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

分析

上述的二叉树一共有n = 7 个节点,那么我们给这个二叉树的每个节点都添加了两个引用,那么我们可以知道 二叉树中引用一共有 2n= 14 个, 有n-1 = 6 个引用是被占用了,那么就还剩下 2n - (n-1) = n + 1 = 8 个空闲的引用.

那么我们如果把这空闲出来的当做线索,也就是我们刚刚改造普通二叉树变成线索二叉树的讲解的方法二,我们就可以知道把这 8 个引用利用起来,分别指向当前节点的直接前驱节点或直接后继节点,这样就完成了线索的添加.

而且我们还可以利用线性结构的二叉树遍历结果知道 :

除第一个节点无直接前驱节点已经最后一个节点无直接后继节点外其余节点一定都有直接前驱节点和直接后继节点.所以 8 个引用中会有两个引用是空的,方别是第一个节点和最后一个节点.

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


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

分析

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


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


设计代码逻辑(重点)

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

  • 代码的逻辑和实现框架概述:
  1. 类结构:
  • ThreadedBinaryTree: 主类,用于实现线索二叉树。
  • BinaryTreeNode: 嵌套静态类,表示二叉树的节点。
  • DataIllegalException: 自定义异常类,用于处理非法数据。
  1. 主要属性:
  • root: 二叉树的根节点。
  • pre: 用于在线索化过程中保存前一个节点。
  • size: 节点的数量。
  1. 构造方法:
  • 无参构造器初始化线索二叉树。
  • 带有根节点参数的构造器,用于创建带有根节点的线索二叉树。
  1. 核心方法:
  • insertNode(): 向线索二叉树中插入新节点。
  • inorderThreaded(): 对二叉树进行中序线索化。
  • inThreadList(): 中序遍历线索化后的二叉树。
  1. 辅助方法:
  • 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)。
  • 空间利用率提升
    传统的二叉树中大量空闲的指针空间得到了有效利用。
  • 遍历过程简化
    不需要维护复杂的栈结构或者进行递归调用,简化了遍历算法的实现。
  • 方便前驱和后继的查找
    对于某些特定的算法或数据结构,如线性化二叉树或转换为双向链表,线索二叉树提供了便捷的支持。

目录
相关文章
|
16天前
|
存储 机器学习/深度学习 算法
C 408—《数据结构》图、查找、排序专题考点(含解析)
408考研——《数据结构》图,查找和排序专题考点选择题汇总(含解析)。
67 29
|
16天前
|
存储 机器学习/深度学习 人工智能
C 408—《数据结构》易错考点200题(含解析)
408考研——《数据结构》精选易错考点200题(含解析)。
90 27
|
3月前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
99 1
|
3月前
|
存储 消息中间件 NoSQL
Redis数据结构:List类型全面解析
Redis数据结构——List类型全面解析:存储多个有序的字符串,列表中每个字符串成为元素 Eelement,最多可以存储 2^32-1 个元素。可对列表两端插入(push)和弹出(pop)、获取指定范围的元素列表等,常见命令。 底层数据结构:3.2版本之前,底层采用**压缩链表ZipList**和**双向链表LinkedList**;3.2版本之后,底层数据结构为**快速链表QuickList** 列表是一种比较灵活的数据结构,可以充当栈、队列、阻塞队列,在实际开发中有很多应用场景。
|
4月前
|
存储 Java 开发者
Java中的Map接口提供了一种优雅的方式来管理数据结构,使代码更加清晰、高效
【10月更文挑战第19天】在软件开发中,随着项目复杂度的增加,数据结构的组织和管理变得至关重要。Java中的Map接口提供了一种优雅的方式来管理数据结构,使代码更加清晰、高效。本文通过在线购物平台的案例,展示了Map在商品管理、用户管理和订单管理中的具体应用,帮助开发者告别混乱,提升代码质量。
47 1
|
3月前
|
存储 NoSQL 关系型数据库
Redis的ZSet底层数据结构,ZSet类型全面解析
Redis的ZSet底层数据结构,ZSet类型全面解析;应用场景、底层结构、常用命令;压缩列表ZipList、跳表SkipList;B+树与跳表对比,MySQL为什么使用B+树;ZSet为什么用跳表,而不是B+树、红黑树、二叉树
|
4月前
|
存储 算法 索引
HashMap底层数据结构及其增put删remove查get方法的代码实现原理
HashMap 是基于数组 + 链表 + 红黑树实现的高效键值对存储结构。默认初始容量为16,负载因子为0.75。当存储元素超过容量 * 负载因子时,会进行扩容。HashMap 使用哈希算法计算键的索引位置,通过链表或红黑树解决哈希冲突,确保高效存取。插入、获取和删除操作的时间复杂度接近 O(1)。
44 0
|
4月前
05(数据结构考研)树相关操作代码
05(数据结构考研)树相关操作代码
40 0
|
3月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
332 9
|
3月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
54 1

热门文章

最新文章

推荐镜像

更多