vlambda博客
学习文章列表

数据结构_012_线索二叉树


线索二叉树

  二叉树的遍历本质上是将一个复杂的非线性结构转换为线性结构,使每个结点都有了唯一前驱和后继(第一个结点无前驱,最后一个结点无后继)。

  对于二叉树的一个结点,查找其左右子女是方便的,其前驱后继只有在遍历中得到。为了容易找到前驱和后继,有两种方法。一是在结点结构中增加向前和向后的指针,这种方法增加了存储开销,不可取;二是利用二叉树的空链指针。

  根据二叉树遍历序列的不同,每个结点的前驱和后继也不同。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种

引自百度百科

线索二叉树

  在含有 个结点的二叉树中,所有结点总共有 个空指针。

  很好理解,因为除开根节点外,其他 个结点都会有来自它父结点的一个分支指向它,二叉树总指针树是 ,所以总共有 个空指针。

  充分利用结点的空指针记录结点的前驱和后继,引入线索二叉树正是为了加快查找结点前驱和后继的速度。

>>左右指针的含义<<

  如果结点有左子结点,那么结点的左指针就指向左子结点,否则指向该结点的前驱。

  如果结点有右子结点,那么结点的右指针就指向右子结点,否则指向该结点的后继。

构造线索二叉树

  二叉树的线索化必须指明是什么遍历顺序的线索化,可分为前序线索二叉树,中序线索二叉树,后序线索二叉树。

  构造线索二叉树,可以使用一个辅助变量pre指针指向当前结点p的按照遍历顺序的上一个结点。也就是说,pre是p的前驱,p是pre的后继。在遍历的过程中,判断p的左指针是否为空,是则将其指向pre。判断pre的右指针是否是空,是则将pre其指向p。

  以中序二叉树为例

>>中序构造线索二叉树<<

结点对象

/**
 * 线索化结点
 */

private static class ThreadedNode<E>{
    /**
     * 结点元素域
     */

    private E element;
    /**
     * 左子树指针
     */

    private ThreadedNode<E> left;
    /**
     * 右子树指针
     */

    private ThreadedNode<E> right;
    /**
     * 0表示指向的是左子结点  默认0
     * 1表示指向前驱结点
     */

    private int leftTag;
    /**
     * 0表示指向是右子结点  默认0
     * 1表示指向后继结点
     */

    private int rightTag;

    // 省略 get set
}

线索化二叉树对象属性

public class ThreadedBinaryTree<E{
    /**
     * 根结点
     */

    private ThreadedNode<E> root;

    /**
     * 线索化二叉树辅助指针,指向当前结点的前驱
     */

    private ThreadedNode<E> pre;
}

初始化一个二叉树

/**
 * 笨比方法 手动创建一个二叉树,直观图如下
 *
 *          曹操
 *        /     \
 *      曹丕     曹植
 *     /   \       \
 *   曹睿   曹协     曹志
 *      \
 *      曹芳
 */

中序线索化该二叉树

/**
 * 中序线索化二叉树
 */

public void inThread() {
    if (root == null) {
        return;
    }
    pre = null;
    inThread(root);
    if (pre != null) {
        // 处理最后一个结点 因为最后一个结点的状态未设置就退出了
        pre.setRightTag(1); // 设置为1是因为最后一个结点的右指针肯定指向null的
        pre.setRight(null);
    }
}

private void inThread(ThreadedNode<E> node) {
    if (node == null) {
        return;
    }
    if (node.getLeft() != null) {
        inThread(node.getLeft());
    }
    if (node.getLeft() == null) {
        // 当前结点的左指针为空,使该指针指向前驱,即pre
        node.setLeft(pre);
        node.setLeftTag(1); // 修改指针类型
    }
    if (pre != null && pre.getRight() == null) {
        // 当前结点的前驱结点的右指针为空,使该指针指向其后驱,即node
        pre.setRight(node);
        pre.setRightTag(1); // 修改指针类型
    }
    pre = node; // 保持pre指向p的前驱
    //        System.out.println(node);
    if (node.getRight() != null) {
        inThread(node.getRight());
    }
}

线索化之后


遍历中序线索二叉树

  当二叉树线索化之后,就不能使用之前的方法来遍历二叉树了,需要使用新的手段来遍历。

递归遍历

  只需要将进入递归的条件改一下即可。

  之前判断是否进入递归是判断当前结点的左右结点司法为空,为空才会去递归。先在需要判断当前结点的左右指针的标志即可,假如标志指针是指向后继结点的,那么就去递归。

/**
 * 中序遍历二叉树  递归
 * @param node
 */

public void inOrder01(ThreadedNode<E> node) {
    if (node == null) {
        return;
    }
    if (node.getLeftTag() != 1) {
        inOrder01(node.getLeft());
    }
    System.out.println(node);
    if (node.getRightTag() != 1) {
        inOrder01(node.getRight());
    }
}

循环遍历1

/**
 * 中序遍历线索二叉树 非递归
 * 思路:
 *  1.首先找到最左边的结点,此结点是中序遍历的第一个结点
 *  2.打印结点
 *  3.因为线索化二叉树的指针可能会指向后继结点,
 *      所以沿着右索引查找当前结点的后继结点并打印,直到rightTag == 0
 *  4.遍历当前结点的右子树
 *  循环往复
 */

public void inOrder02() {
    ThreadedNode<E> node = root; // 获取根结点
    while (node != null) {
        // 首先找到最左边的结点
        while (node.getLeftTag() == 0) {
            node = node.getLeft();
        }
        // 打印结点
        System.out.println(node);
        // 判断当前结点的右指针是否指向后继结点
        while (node.getRightTag() == 1 && node.getRight() != null) {
            // 循环条件,右指针指向的结点是当前结点的后继结点,直接打印
            node = node.getRight(); // 访问后继结点
            System.out.println(node);
        }
        // 遍历右子树
        node = node.getRight();
    }
}

循环遍历2

  其实思路和上面的那种一样,只不过是换了个形式。

思路:

  我们可以把二叉树的中序的序列看成是一个链表,每次都去查找后继结点,按照链表的方式遍历。

  1、首先找到中序遍历的第一个结点。方法是:中序遍历的第一个结点是从根节点一直往左找,最左的结点就是了。

  2、然后就是找某个结点的后继,方法是:

  假如右指针是线索,则其指向的结点是后继结点,直接打印。

  假如右指针是指向右子结点,则其右子树的最左侧结点就是后驱结点。

/**
 * 中序遍历线索二叉树  非递归
 */

public void inOrder03() {
    ThreadedNode<E> node = root;
    for(node = firstNode(node); node != null; node = nextNode(node)) {
        System.out.println(node);
    }
}

/**
 * 找到某个结点 中序遍历的 第一个访问的结点
 * @param node
 * @return
 */

private ThreadedNode<E> firstNode(ThreadedNode<E> node) {
    if (node == null) {
        return null;
    }
    // 直到找到结点的左指针标记为1的,说明就是最左结点了(但是不一定是叶子结点)
    while (node.getLeftTag() == 0) {
        node = node.getLeft();
    }
    return node;
}

/**
 * 找到某个结点的下一个结点
 * 假如当前结点的rightTag标志为 1 说明其指向后继结点 直接返回
 * 假如当前结点的rightTag标志为 0 说明其指向右子结点 返回右子结点的最左结点
 *
 * @param node
 * @return
 */

private ThreadedNode<E> nextNode(ThreadedNode<E> node) {
    if (node == null) {
        return null;
    }
    if (node.getRightTag() == 1) {
        return node.getRight();
    } else {
        // 访问右结点的 后继   右子树最左下的结点
        return firstNode(node.getRight());
    }
}

反向遍历中序线索二叉树

  反向遍历中序线索二叉树,思路和正序是相反的。

思路:

  1、先找到中序遍历的最后一个结点,从根结点开始往右找,最右侧的结点就是中序序列的最后一个结点了。

  2、找当前结点的前驱:

  假如左指针是线索,则其指向结点为前驱结点,直接返回。

  假如左指针指向的左子结点,则其左子树的最右侧结点为其前驱结点。

/**
 * 反向中序遍历 线索二叉树
 */

public void inOrderReverse() {
    ThreadedNode<E> node = root;
    for(node = lastNode(node); node != null; node = preNode(node)) {
        System.out.println(node);
    }
}

/**
 * 找到中序最后一个结点
 * 中序遍历的 最后一个结点 是 最右边的结点  不一定是叶子结点
 */

private ThreadedNode<E> lastNode(ThreadedNode<E> node) {
    if (node == null) {
        return null;
    }
    while (node.getRightTag() == 0) {
        node = node.getRight();
    }
    return node;
}

/**
 * 找到中序遍历  的上一个节点
 * 假如当前结点的leftTag标志为 1 说明其指向前驱结点 直接返回
 * 假如当前结点的leftTag标志为 0 说明其指向左子结点 返回左子结点的最右结点
 */

private ThreadedNode<E> preNode(ThreadedNode<E> node) {
    if (node == null) {
        return null;
    }
    if (node.getLeftTag() == 1) {
        return node.getLeft();
    } else {
        return lastNode(node.getLeft());
    }
}

先序和后序索引二叉树

  这两种二叉树的创建和中序索引二叉树的创建代码基本一样,只是线索化部分的代码的位置不一样。

先序线索二叉树找后继:

  假如某个结点有左子结点,那么左子结点就是其后继。

  假如某个结点没有左子结点但是有右子结点,那么右子结点就是其后继。

  假如某个结点是叶子结点,那么右指针指向的结点就是其后继。

后续线索二叉树找后继:

重点是要利用父结点,可以使用存三个指针的二叉树,多一个指向父节点的指针。

  假如是根结点,后继为null,因为是最后一个结点。

  假如某个结点是其父结点的右子结点,或者是其父结点的左子结点且其父结点没有右子树,则其后继是其父结点。

  假如某个结点是其父结点的左子结点,且父结点有右子结点,则其后继是父结点的右子树上后序遍历的第一个结点。

小结

  对于频繁查找前驱和后继的计算,线索二叉树优于普通二叉树。

  但是对于插入和删除操作,线索二叉树比普通二叉树开销大,因为除插入和删除操作外,还要修改相应的线索。