数据结构与算法--线索二叉树及其前序、中序遍历

数据结构与算法--线索二叉树及其前序、中序遍历

二叉树如果某个结点没有左孩子或右孩子,则这个域就为空。如node.lChild = null

而叶子结点两个指针域都是null。我们知道n个结点的二叉树共有2n个指针域,树只有n-1条分支,也就是说还有2n - (n -1) = n + 1个域是空指针。为了充分利用这些空指针,可以存一些有用的信息——比如某结点的前驱和后继结点(按某种次序遍历后的顺序)。这种指向前序后继的指针称为线索,具有前驱后继关系的二叉树叫线索二叉树。线索二叉树的好处是:不仅节约了空间,而且一旦按照某种次序(中序、前序或后序)遍历一次后,线索信息就已经初始化好,下次想要获取某个结点的前驱后继就不用再遍历整棵树了。

具体来说,对于所有空指针域,lChild指向该结点的前驱结点;rChild指向该结点的后继结点。但是lChild和rChild同时也表示某结点的左孩子和右孩子,一个指针有两种意思,对某个结点究竟指代的哪种意思?显然需要一个标志加以区分,我们给每个结点的孩子加上标志,如果是false则表示左/右孩子,如果是true则表示前驱/后继。这棵二叉树中序遍历的结果是HDIBEAFCG,按照遍历所得顺序,H的后继就是D,h.rChild本来是null,现在让其指向D,由于这表示后继,所以相应的标志位应该设置为true。这幅图将所有rChild为空的指针域都指向了其后继。

那么前驱呢?自然是将所有lChild为空的指针域指向该结点的前驱。如下图。

如果将这两幅图结合起来,则每个空指针域都已用上了。这棵二叉树就成了下面这个样子

虚线表示右孩子或者后继,实线表示左孩子或者前驱,具体怎么区分也就是看给结点左右孩子设置的标志位了。仔细观察,把这颗树两头拉直,就成了双向链表!(但又和普通的双向链表不同,这里前驱后继的含义指代比较模糊)

现在来试着实现一下。

package Chap6;

public class ThreadTree<Item> {
    public static class Node<T> {
        private T data;
        private Node<T> lchild;
        private Node<T> rchild;
        private boolean isleftThead;
        private boolean isRightThead;

        public Node(T data) {
            this.data = data;
            isleftThead = false;
            isRightThead = false;
        }

        public T getData() {
            return data;
        }

        public Node<T> getLchild() {
            return lchild;
        }

        public Node<T> getRchild() {
            return rchild;
        }

        @Override
        public String toString() {
            String lchildInfo = lchild == null ? null : lchild.getData().toString();
            String rchildInfo = rchild == null ? null : rchild.getData().toString();

            return "Node{" +
                    "data=" + data +
                    ", lchild=" + lchildInfo +
                    ", rchild=" + rchildInfo +
                    '}';
        }
    }

    private Node<Item> root;
    private Node<Item> preNode;
    private int nodesNum;

    public void setRoot(Item data) {
        root = new Node<>(data);
        nodesNum++;
    }

    public void addLeftChild(Item data, Node<Item> parent) {
        parent.lchild = new Node<>(data);
        nodesNum++;
    }

    public void addRightChild(Item data, Node<Item> parent) {
        parent.rchild = new Node<>(data);
        nodesNum++;
    }

    public Node<Item> root() {
        return root;
    }

    /**
     * 中序遍历线索化二叉树
     */
    public void inOrderThread(Node<Item> node) {
        if (node == null) {
            return;
        }
        inOrderThread(node.lchild);

        if (node.lchild == null) {
            node.lchild = preNode;
            node.isleftThead = true;
        }

        if (preNode != null && preNode.rchild == null) {
            preNode.rchild = node;
            preNode.isRightThead = true;
        }
        // preNode始终表示上一个访问的结点
        preNode = node;
        inOrderThread(node.rchild);
    }

    public void inOrderThread() {
        inOrderThread(root);
    }

    public void inOrderTraversal(Node<Item> node) {
        // 不断深入左子树,只要某个结点左孩子为空,则标志位肯定为true
        while (node != null) {
            while (!node.isleftThead) {
                node = node.lchild;
            }

            System.out.print(node.getData() + " ");
            while (node.isRightThead && node.rchild != null) {
                node = node.rchild;
                System.out.print(node.getData() + " ");
            }
            node = node.rchild;
        }
    }

    public void inOrderTraversal() {
        inOrderTraversal(root);
    }

    /**
     * 前序遍历线索化二叉树
     */
    public void preOrderThread(Node<Item> node) {
        if (node == null) {
            return;
        }

        if (node.lchild == null) {
            node.lchild = preNode;
            node.isleftThead = true;
        }

        if (preNode != null && preNode.rchild == null) {
            preNode.rchild = node;
            preNode.isRightThead = true;
        }
        // preNode始终表示上一个访问的结点
        preNode = node;
        // 这里需要判断,因为node.lchild和node.rchild可能已经被设置了标志。若还递归就会打乱了已设置好的标志位,而且还会StackOverflow
        // 而中序遍历递归是,标志位均未被设置,所以无需判断
        if (!node.isleftThead) {
            preOrderThread(node.lchild);
        }
        if (!node.isRightThead) {
            preOrderThread(node.rchild);
        }
    }

    public void preOrderThread() {
        preOrderThread(root);
    }

    public void preOrderTraversal(Node<Item> node) {
        while (node != null) {
            while (!node.isleftThead) {
                System.out.print(node.getData() + " ");
                node = node.lchild;
            }

            System.out.print(node.getData() + " ");
            node = node.rchild;
        }
    }

    public void preOrderTraversal() {
        preOrderTraversal(root);
    }

    public static void main(String[] args) {
        ThreadTree<String> tree = new ThreadTree<>();
        tree.setRoot("A");
        Node<String> root = tree.root();
        tree.addLeftChild("B", root);
        tree.addRightChild("C", root);
        tree.addLeftChild("D", root.getLchild());

        tree.addLeftChild("E", root.getRchild());
        tree.addRightChild("F", root.getRchild());
        tree.addLeftChild("G", root.getLchild().getLchild());
        tree.addRightChild("H", root.getLchild().getLchild());
        tree.addRightChild("I", root.getRchild().getLchild());

        System.out.println("中序线索化并遍历");
        tree.inOrderThread();
        tree.inOrderTraversal();

        // 线索化只能调用一次!!!一旦设置好,就不要去打乱了。所以想运行上面的需要注释掉下面的
//        System.out.println("前序线索化并遍历");
//        tree.preOrderThread();
//        tree.preOrderTraversal();
    }
}

首先是Node类增加了标志位isleftThead和isRightThread,含义上面解释过了。然后是ThreadTree类除了有个root成员,还新增了preNode成员,专门表示在线索化中上一个刚访问过的结点。部分方法直接使用了普通二叉树的代码。这里着重说一下线索化的代码。前序和后序线索化及遍历都比较简单,后序复杂一点我也懒得去实现了。

中序线索化及中序遍历

之所以称为中序线索化,是因为它和普通二叉树中序遍历的递归实现,代码结构几乎一样。看单独抽取出来的中序线索化实现,只要把中序遍历递归实现中的打印操作换成注释了begin和end之间的代码就OK了。

public void inOrderThread(Node<Item> node) {
    if (node == null) {
        return;
    }
    inOrderThread(node.lchild);
    // begin
    if (node.lchild == null) {
      node.lchild = preNode;
      node.isleftThead = true;
    }

    if (preNode != null && preNode.rchild == null) {
        preNode.rchild = node;
        preNode.isRightThead = true;
    }
    // preNode始终表示上一个访问的结点
    preNode = node;
    // end
    inOrderThread(node.rchild);
}

中间的代码做的事主要是:

  • 某结点的左孩子为空,设置标志位为true,且让上一个结点成为当前结点的前驱
  • 某结点的右孩子为空,设置标志位为true,且让当前结点成为前一个结点的后继。这里因为要调用preNode的方法,所以要进行判空操作。
  • 当前结点访问完,即将访问下一个结点了,于是当前结点也就成了上一个结点preNode。

这里设置某结点的后继之所以使用preNode,理由很简单,当前结点的后继还没访问到呢,只能用已经访问过的前一个结点,它的后继可能是当前结点。

再来看中序遍历。

public void inOrderTraversal(Node<Item> node) {
    // 不断深入左子树,直到遇见第一个线索
    while (node != null) {
        while (!node.isleftThead) {
            node = node.lchild;
        }

      System.out.print(node.getData() + " ");
      while (node.isRightThead && node.rchild != null) {
          node = node.rchild;
          System.out.print(node.getData() + " ");
      }
      node = node.rchild;
    }
}

先是从根结点开始按照左子树深入,直到遇见第一个左孩子是线索的结点,紧接着就打印它,这次打印的其实是链表头。接下来看它的右孩子是不是后继,如果是就继续打印;直到右孩子不是线索,此时转到右子树,开始和根结点一样的循环...最后一个while中需要判断node.rchild不为空,如果为空,我们打印出来就是null,这不是我们想要看得结果。

前序线索化

把设置标志那一团代码放到递归左右子树之前,就成了前序线索化。特别注意,在递归前判断了是否为线索结点。因为前序线索化中,递归发生在设置标志位之后,所以node.lchildnode.rchild可能已经被设置了标志。若还递归就会打乱了已设置好的标志位,而且还会发生StackOverflow。而中序线索化中无需判断是因为,递归左子树发生在设置左孩子的线索之前,而右孩子的线索的设置是针对上一个结点的,当前结点的右孩子并没有线索化,所以对当前结点的右子树node.rchild的递归并没有影响。

if (!node.isleftThead) {
    preOrderThread(node.lchild);
}
if (!node.isRightThead) {
    preOrderThread(node.rchild);
}

接着看前序遍历的。还是根结点开始,深入左子树,只要没遇到左孩子是线索的结点,就立即打印。然后跳出while循环打印第一个左孩子是线索的结点。然后转向其后继或者是右子树,继续大循坏...

如果我们经常需要遍历二叉树,并且要知道某结点的前驱后继信息,那么使用线索二叉树是个不错的选择。特别注意一点,线索化只能执行一次,已经设置好的标志信息就不要再设置第二次了(除非清空标志位),否则会导致标志位的混乱,导致遍历失败。


by @sunhaiyu

2017.9.14

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • 1.树(Tree): 树是 n(n>=0) 个结点的有限集。当 n=0 时称为空树。在任意一颗非空树中:有且仅有一...
    ql2012jz阅读 998评论 0 3
  • 四、树与二叉树 1. 二叉树的顺序存储结构 二叉树的顺序存储就是用数组存储二叉树。二叉树的每个结点在顺序存储中都有...
    MinoyJet阅读 1,512评论 0 7
  • 原链接:理解线索二叉树|CloudWong 线索二叉树原理 遍历二叉树的其实就是以一定规则将二叉树中的结点排列成一...
    简Cloud阅读 8,382评论 1 16
  • 前面讲到的顺序表、栈和队列都是一对一的线性结构,这节讲一对多的线性结构——树。「一对多」就是指一个元素只能有一个前...
    Alent阅读 2,229评论 1 28
  • 概念 树是什么 树(Tree)是n(n>=0)个结点的有限集。 n = 0的树是空树。 在任意一棵非空树中: 有且...
    刚刚悟道阅读 5,035评论 1 16