【数据结构】初入数据结构的线索二叉树以及Java代码实现

初入数据结构的线索二叉树以及Java代码实现


  • 前提概念
    • 什么是线索二叉树
    • 线索二叉树的由来
    • 线索二叉树相比普通二叉树的好处
  • 线索二叉树
    • 普通二叉树中序线索化
    • 线索二叉树中序遍历
  • Java代码实现

前提概念


什么是线索二叉树?

我们知道二叉树的是一棵树的度小于等于2有序树;那么线索二叉树又是什么呢?其实线索二叉树实际是一棵变形的二叉树。

如果有一种算法需要经常的对一棵二叉树进行遍历,那么遍历的过程就是在频繁的利用递归或栈做重复性的操作,而线索二叉树不需要如此,通过使用二叉树空闲的内存空间记录某些结点的前趋和后继结点,在遍历的时候,就可以利用好这些保存的结点信息,提高遍历效率。

那么使用这种方式(利用好二叉树空闲内存的方式)构建的二叉树就是一颗线索二叉树

线索二叉树的由来

普通二叉树中存在指针浪费的问题:
我们知道线索二叉树相比普通二叉树而言,就是利用了普通二叉树的空闲内存,记录了某些结点的前趋和后继元素信息。我们先来了解一个前提,这个空闲内存到底指的是什么呢?

我们看到上面的图,该树就是一颗很普通的二叉树,结点结构也很简单,就是数据域,左孩子指针,右孩子指针。该树有7个结点,所以该树就有2*7 = 14个指针空间,这14个指针空间就是我们所说的内存空间。

而空闲的内存空间又指的是什么呢?很简单,就是14个指针空间中指向null的空间,比如D,E,F结点的左右孩子指针和C结点的左孩子指针。因为这些空间并没有指向左右孩子,本质上就是一种浪费。本着有好东西就不能浪费的理念,所以就出现了线索二叉树

空闲指针的规律:

  • 存在n个结点的二叉链表必定存在n + 1个空指针域
  • 不要问为什么,数一下就知道了

所以线索二叉树实际上就是一棵利用普通二叉树剩下的这些空指针域去存储结点之间的前趋和后继关系的特殊二叉树

线索二叉树相比普通二叉树的好处

  • 更好的利用了普通二叉树的空指针域,相比普通二叉树,并没有增加结构开销
  • 在进行二叉树遍历的时候,利用好前趋结点和后继结点的信息,可以更快的进行前,中,后序等遍历

线索二叉树


线索二叉树的结点结构

线索二叉树的结点结构:
我们知道,通常二叉树在代码实现中,是通过二叉链的形式去表达的,通常都是一个数据域,两个指针域

但我们知道,在线索二叉树中,如果结点有左孩子,那么lchild就会指向左孩子,否则lchild就会指向该结点的直接前趋结点;同样,如果该结点有右孩子,那么rchild就会指向右孩子,否则rchild就会指向该结点的直接后继结点。

也就是说,左右孩子指针是有双重意义的,这很容易让人产生迷惑,所以为了避免指针域指向结点的意义混淆,需要在普通二叉树的结点构造上做一些小小的改变,增加两个标志位

  • 我们在原有的基础上,增加了两个标志位
  • ltag值为0时,表示lchild指向的是该结点的左孩子;为1的时候,表示lchild指向的是该结点的直接前趋结点
  • rtag值为0时,表示rchild指向的是该结点的右孩子;为1的时候,表示rchild指向的是该结点的直接后继结点

结点代码:

 public static class TreeNode<E> {

        /**
         * 数据域
         */
        private E data;
        /**
         * 子结点域
         */
        private TreeNode<E> lchild, rchild;
        /**
         * tag域
         * ltag为0时,表示lchild指向的是左孩子,如果ltag为1时,表示指向的是直接前趋结点
         * rtag为0时,表示rchild指向的是右孩子,如果rtag为1时,表示指向的是直接后继结点
         */
        private int ltag, rtag;
 }

普通二叉树中序线索化

什么是普通二叉树线索化?就是给定一棵普通的二叉树,假如有n个结点,那么这棵树必然有2n个指针域,n-1个空指针域。通过中序的方式线索化就是,通过中序遍历的顺序依次将该树的空指针域指向前趋或后继结点,填充空指针域,让一棵普通二叉树变成一棵

/**
     * 通过中序线索化一颗普通二叉树
     * 让其从普通二叉树变成线索二叉树
     */
    public void createThreadTreeByMidOrder() {
        if (this.rootNode == null) {
            return;
        }
        createThreadTreeByMidOrder(this.rootNode);

    }

    /**
     * 通过中序线索化一颗普通二叉树
     * 让其从普通二叉树变成线索二叉树
     * 逻辑就跟递归实现的中序遍历差不多
     */
    private void createThreadTreeByMidOrder(TreeNode<T> root) {

        //递归推出条件
        if (root == null) {
            return;
        }

        /**
         * 1.先递归左子树
         */
        createThreadTreeByMidOrder(root.lchild);

        /**
         * 2. 再到相对根节点
         * 一开始的前趋后继链的第一个前趋结点必然为空
         */
        //如果当前结点的左孩子等于null,则代表它是空指针域
        if (root.lchild == null) {
            //设置前趋结点和状态,前趋结点是一个临时全局变量
            root.ltag = 1;
            root.lchild = preNode;
        }

        //如果前趋结点不为null(不是链表头),就看前趋结点的右孩子等不等于null,如果是null,则填写后继结点
        if (preNode != null && preNode.rchild == null) {
            //前趋结点的右孩子为后继结点,后继结点是当前结点 | 要区分前趋结点和当前结点分别是什么,且该前趋是当前结点的前趋
            preNode.rtag = 1;
            preNode.rchild = root;
        }

        //每处理完一个结点,当前root结点就是下一个结点的前趋结点
        preNode = root;

        /**
         * 3. 递归右子树
         */
        createThreadTreeByMidOrder(root.rchild);

    }
  • 代码实现其实也不难,整体的中序线索化就是一个递归的中序遍历
  • 递归推出条件就是,相对根结点==null
  • 需要有一个全局的临时变量记录线索后的每一步的前趋结点preNode, 默认初始为null
  • 而每次到了遍历相对根结点阶段,就需要做两个步骤
    1. 首先找到最左叶子结点A的左指针,它必然是空指针域,指向null, 这个最左子结点A就是的当前相对根结点,其ltag变为1,代表lchild指向前趋结点,非左孩子。同时结点A的lchild指向临时变量preNode(第一次必然为null)
    2. 然后判断当前相对根结点前趋结点preNode是否为null, 且preNode的右指针rchild是否也是空指针域。为什么要判断前趋结点的右指针呢?因为每一轮遍的第1步都是先解决左指针,而过了这轮遍历,下一轮开始,上轮的当前结点就会成为本轮的前趋结点, 我们就要解决上一轮相对根结点(本轮的前趋结点)的右指针rchild ; 即本轮解决当前结点A的左指针,下一轮就会解决本轮前趋结点(结点A)的右指针
    3. 每一轮结束,当前相对结点就会赋值给临时全局遍历前趋结点preNode

线索二叉树中序线索遍历

什么是线索二叉树的中序遍历呢?普通二叉树的遍历,我们都知道,左->根->右;但是线索二叉树的中序遍历还需要这样子吗?那必须有更加高效的方式啦!

/**
     * 中序遍历线索二叉树 | 迭代方式
     * 1. 找到最左叶子节点,然后向右开始线索遍历,找后继结点
     * 2. 因为有中断结点,所以无法一直都向右遍历,因为中断结点本身有右子树,所以无法找到后继结点
     * 3. 因为中断结点的影响,所以需要找到右孩子,跳到第一层while,重写开始循环,即迭代子树
     */
    public void midOrderWithIterator() {
        //如果是空树,直接返回
        if (this.rootNode == null) {
            return;
        }

        //从根节点开始
        TreeNode<T> node = this.rootNode;

        //只要当前节点不为null,就一直遍历
        while (node != null) {
            //找到最左叶子结点,ltag == 1就退出循环
            while (node.ltag == 0) {
                node = node.lchild;
            }
            //输出
            System.out.println(node.data);
            //只要当前结点不是中断结点,那么直接输出可遍历到的后继结点
            while (node.rtag == 1 && node.rchild != null) {
                node = node.rchild;
                System.out.println(node.data);
            }
            //如果被打破了条件,证明链表中断,当前node结点是中断结点,直接找中断结点的右孩子,然新的结点成为根结点,重新开始迭代,遍历子树
            node = node.rchild;
        }
    }
  • 找到最左叶子节点,然后向右开始线索遍历,找后继结点
  • 因为有中断结点,所以无法一直都向右遍历,因为中断结点本身有右子树,所以无法找到后继结点
  • 因为中断结点的影响,所以需要找到右孩子,跳到第一层while,重写开始循环,即迭代子树

这里盗个图来自@梦想拒绝零风险
在这里插入图片描述

  • 从上图看,结点1就是本树的最左叶子结点,它的ltag必然为1,所以会打破第一层内循环
  • 而结点2就是一个中断结点,它的左右指针都是指向左右孩子,并不属于原指针域,此时我们能做的就只是找到她的右孩子,重新开始一轮外循环
  • 总结起来就是,有两层循环,一层外循环,一层内循环,内循环有两个;外循环是为了解决链表中断,出现中断结点,那就以中断结点的右孩子作为一棵新的树,重新遍历;第一层内循环的目的就是找到这个树的最左叶子结点,第二层内循环就是为了顺着后继结点一直遍历,只要发现链表中断,就打破第二层循环,重新开始外循环

Java代码实现


主要功能:

  • 中序将普通二叉树线索化 -> 线索二叉树
  • 中序遍历一棵线索二叉树

TreeNode(线索二叉树结点)

 public class TreeNode<E> {

        /**
         * 数据域
         */
        private E data;
        /**
         * 子结点域
         */
        private TreeNode<E> lchild, rchild;
        /**
         * tag域
         * ltag为0时,表示lchild指向的是左孩子,如果ltag为1时,表示指向的是直接前趋结点
         * rtag为0时,表示rchild指向的是右孩子,如果rtag为1时,表示指向的是直接后继结点
         */
        private int ltag, rtag;

        public TreeNode(E data) {
            this.data = data;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof TreeNode)) return false;

            TreeNode<?> treeNode = (TreeNode<?>) o;

            if (ltag != treeNode.ltag) return false;
            if (rtag != treeNode.rtag) return false;
            if (data != null ? !data.equals(treeNode.data) : treeNode.data != null) return false;
            if (lchild != null ? !lchild.equals(treeNode.lchild) : treeNode.lchild != null) return false;
            return rchild != null ? rchild.equals(treeNode.rchild) : treeNode.rchild == null;
        }

        @Override
        public int hashCode() {
            int result = data != null ? data.hashCode() : 0;
            result = 31 * result + (lchild != null ? lchild.hashCode() : 0);
            result = 31 * result + (rchild != null ? rchild.hashCode() : 0);
            result = 31 * result + ltag;
            result = 31 * result + rtag;
            return result;
        }
    }


ThreadTree(线索二叉树)

/**
 * 线索二叉树
 *
 * @author liwenjie
 */
public class ThreadTree<T> {

   
    /**
     * 根节点
     */
    private TreeNode<T> rootNode;
    /**
     * 前驱节点 | 临时存放,方法的公共变量
     */
    private TreeNode<T> preNode;

    /**
     * 构造只有一个根节点的树
     *
     * @param rootNode
     */
    public ThreadTree(TreeNode<T> rootNode) {
        this.rootNode = rootNode;
    }

    /**
     * 为某个节点添加左孩子
     *
     * @param parent
     */
    public void addLChild(TreeNode<T> parent, TreeNode<T> lchild) {
        if (this.rootNode == null || parent == null || parent.lchild != null) {
            return;
        }
        parent.lchild = lchild;
    }

    /**
     * 为某个节点添加右孩子
     *
     * @param parent
     */
    public void addRChild(TreeNode<T> parent, TreeNode<T> rchild) {
        if (this.rootNode == null || parent == null || parent.rchild != null) {
            return;
        }
        parent.rchild = rchild;
    }

    /**
     * 通过中序线索化一颗普通二叉树
     * 让其从普通二叉树变成线索二叉树
     */
    public void createThreadTreeByMidOrder() {
        if (this.rootNode == null) {
            return;
        }
        createThreadTreeByMidOrder(this.rootNode);

    }

    /**
     * 通过中序线索化一颗普通二叉树
     * 让其从普通二叉树变成线索二叉树
     * 逻辑就跟递归实现的中序遍历差不多
     */
    private void createThreadTreeByMidOrder(TreeNode<T> root) {


        //递归推出条件
        if (root == null) {
            return;
        }

        /**
         * 1.先递归左子树
         */

        createThreadTreeByMidOrder(root.lchild);

        /**
         * 2. 再到相对根节点
         * 一开始的前趋后继链的第一个前趋结点必然为空
         */
        //如果当前结点的左孩子等于null,则代表它是空指针域
        if (root.lchild == null) {
            //设置前趋结点和状态,前趋结点是一个临时全局变量
            root.ltag = 1;
            root.lchild = preNode;
        }

        //如果前趋结点不为null(不是链表头),就看前趋结点的右孩子等不等于null,如果是null,则填写后继结点
        if (preNode != null && preNode.rchild == null) {
            //前趋结点的右孩子为后继结点,后继结点是当前结点 | 要区分前趋结点和当前结点分别是什么,且该前趋是当前结点的前趋
            preNode.rtag = 1;
            preNode.rchild = root;
        }

        //每处理完一个结点,当前root结点就是下一个结点的前趋结点
        preNode = root;

        /**
         * 3. 递归右子树
         */
        createThreadTreeByMidOrder(root.rchild);

    }


    /**
     * 中序遍历线索二叉树 | 迭代方式
     * 1. 找到最左叶子节点,然后向右开始线索遍历,找后继结点
     * 2. 因为有中断结点,所以无法一直都向右遍历,因为中断结点本身有右子树,所以无法找到后继结点
     * 3. 因为中断结点的影响,所以需要找到右孩子,跳到第一层while,重写开始循环,即迭代子树
     */
    public void midOrderWithIterator() {
        //如果是空树,直接返回
        if (this.rootNode == null) {
            return;
        }

        //从根节点开始
        TreeNode<T> node = this.rootNode;

        //只要当前节点不为null,就一直遍历
        while (node != null) {
            //找到最左叶子结点,ltag == 1就退出循环
            while (node.ltag == 0) {
                node = node.lchild;
            }
            //输出
            System.out.println(node.data);
            //只要当前结点不是中断结点,那么直接输出可遍历到的后继结点
            while (node.rtag == 1 && node.rchild != null) {
                node = node.rchild;
                System.out.println(node.data);
            }
            //如果被打破了条件,证明链表中断,当前node结点是中断结点,直接找中断结点的右孩子,然新的结点成为根结点,重新开始迭代,遍历子树
            node = node.rchild;
        }
    }

    public static void main(String[] args) {
        TreeNode<Integer> node1 = new TreeNode<>(1);
        TreeNode<Integer> node2 = new TreeNode<>(2);
        TreeNode<Integer> node3 = new TreeNode<>(3);
        TreeNode<Integer> node4 = new TreeNode<>(4);
        TreeNode<Integer> node5 = new TreeNode<>(5);
        TreeNode<Integer> node6 = new TreeNode<>(6);
        TreeNode<Integer> node7 = new TreeNode<>(7);
        ThreadTree<Integer> threadTree = new ThreadTree<>(node1);
        threadTree.addLChild(node1, node2);
        threadTree.addRChild(node1, node3);
        threadTree.addLChild(node2, node4);
        threadTree.addRChild(node2, node5);
        threadTree.addLChild(node3, node6);
        threadTree.addRChild(node3, node7);

        threadTree.createThreadTreeByMidOrder();
        threadTree.midOrderWithIterator();


    }


}

参考资料


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章