數(shù)據(jù)結(jié)構(gòu)與算法--線索二叉樹及其前序屑那、中序遍歷

數(shù)據(jù)結(jié)構(gòu)與算法--線索二叉樹及其前序、中序遍歷

二叉樹如果某個(gè)結(jié)點(diǎn)沒(méi)有左孩子或右孩子,則這個(gè)域就為空持际。如node.lChild = null沃琅,

而葉子結(jié)點(diǎn)兩個(gè)指針域都是null。我們知道n個(gè)結(jié)點(diǎn)的二叉樹共有2n個(gè)指針域选酗,樹只有n-1條分支阵难,也就是說(shuō)還有2n - (n -1) = n + 1個(gè)域是空指針岳枷。為了充分利用這些空指針芒填,可以存一些有用的信息——比如某結(jié)點(diǎn)的前驅(qū)和后繼結(jié)點(diǎn)(按某種次序遍歷后的順序)。這種指向前序后繼的指針?lè)Q為線索空繁,具有前驅(qū)后繼關(guān)系的二叉樹叫線索二叉樹殿衰。線索二叉樹的好處是:不僅節(jié)約了空間,而且一旦按照某種次序(中序盛泡、前序或后序)遍歷一次后闷祥,線索信息就已經(jīng)初始化好,下次想要獲取某個(gè)結(jié)點(diǎn)的前驅(qū)后繼就不用再遍歷整棵樹了傲诵。

具體來(lái)說(shuō)凯砍,對(duì)于所有空指針域,lChild指向該結(jié)點(diǎn)的前驅(qū)結(jié)點(diǎn)拴竹;rChild指向該結(jié)點(diǎn)的后繼結(jié)點(diǎn)悟衩。但是lChild和rChild同時(shí)也表示某結(jié)點(diǎn)的左孩子和右孩子,一個(gè)指針有兩種意思栓拜,對(duì)某個(gè)結(jié)點(diǎn)究竟指代的哪種意思座泳?顯然需要一個(gè)標(biāo)志加以區(qū)分,我們給每個(gè)結(jié)點(diǎn)的孩子加上標(biāo)志幕与,如果是false則表示左/右孩子挑势,如果是true則表示前驅(qū)/后繼。這棵二叉樹中序遍歷的結(jié)果是HDIBEAFCG啦鸣,按照遍歷所得順序潮饱,H的后繼就是D,h.rChild本來(lái)是null诫给,現(xiàn)在讓其指向D饼齿,由于這表示后繼,所以相應(yīng)的標(biāo)志位應(yīng)該設(shè)置為true蝙搔。這幅圖將所有rChild為空的指針域都指向了其后繼缕溉。

那么前驅(qū)呢?自然是將所有l(wèi)Child為空的指針域指向該結(jié)點(diǎn)的前驅(qū)吃型。如下圖证鸥。

如果將這兩幅圖結(jié)合起來(lái),則每個(gè)空指針域都已用上了。這棵二叉樹就成了下面這個(gè)樣子

虛線表示右孩子或者后繼枉层,實(shí)線表示左孩子或者前驅(qū)泉褐,具體怎么區(qū)分也就是看給結(jié)點(diǎn)左右孩子設(shè)置的標(biāo)志位了。仔細(xì)觀察鸟蜡,把這顆樹兩頭拉直膜赃,就成了雙向鏈表!(但又和普通的雙向鏈表不同揉忘,這里前驅(qū)后繼的含義指代比較模糊)

現(xiàn)在來(lái)試著實(shí)現(xiàn)一下跳座。

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始終表示上一個(gè)訪問(wèn)的結(jié)點(diǎn)
        preNode = node;
        inOrderThread(node.rchild);
    }

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

    public void inOrderTraversal(Node<Item> node) {
        // 不斷深入左子樹,只要某個(gè)結(jié)點(diǎn)左孩子為空,則標(biāo)志位肯定為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始終表示上一個(gè)訪問(wèn)的結(jié)點(diǎn)
        preNode = node;
        // 這里需要判斷泣矛,因?yàn)閚ode.lchild和node.rchild可能已經(jīng)被設(shè)置了標(biāo)志疲眷。若還遞歸就會(huì)打亂了已設(shè)置好的標(biāo)志位,而且還會(huì)StackOverflow
        // 而中序遍歷遞歸是您朽,標(biāo)志位均未被設(shè)置狂丝,所以無(wú)需判斷
        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();

        // 線索化只能調(diào)用一次!;┳堋几颜!一旦設(shè)置好,就不要去打亂了讯屈。所以想運(yùn)行上面的需要注釋掉下面的
//        System.out.println("前序線索化并遍歷");
//        tree.preOrderThread();
//        tree.preOrderTraversal();
    }
}

首先是Node類增加了標(biāo)志位isleftThead和isRightThread蛋哭,含義上面解釋過(guò)了。然后是ThreadTree類除了有個(gè)root成員耻煤,還新增了preNode成員具壮,專門表示在線索化中上一個(gè)剛訪問(wèn)過(guò)的結(jié)點(diǎn)。部分方法直接使用了普通二叉樹的代碼哈蝇。這里著重說(shuō)一下線索化的代碼棺妓。前序和后序線索化及遍歷都比較簡(jiǎn)單,后序復(fù)雜一點(diǎn)我也懶得去實(shí)現(xiàn)了炮赦。

中序線索化及中序遍歷

之所以稱為中序線索化怜跑,是因?yàn)樗推胀ǘ鏄渲行虮闅v的遞歸實(shí)現(xiàn),代碼結(jié)構(gòu)幾乎一樣吠勘⌒苑遥看單獨(dú)抽取出來(lái)的中序線索化實(shí)現(xiàn),只要把中序遍歷遞歸實(shí)現(xiàn)中的打印操作換成注釋了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始終表示上一個(gè)訪問(wèn)的結(jié)點(diǎn)
    preNode = node;
    // end
    inOrderThread(node.rchild);
}

中間的代碼做的事主要是:

  • 某結(jié)點(diǎn)的左孩子為空植锉,設(shè)置標(biāo)志位為true,且讓上一個(gè)結(jié)點(diǎn)成為當(dāng)前結(jié)點(diǎn)的前驅(qū)峭拘。
  • 某結(jié)點(diǎn)的右孩子為空俊庇,設(shè)置標(biāo)志位為true狮暑,且讓當(dāng)前結(jié)點(diǎn)成為前一個(gè)結(jié)點(diǎn)的后繼。這里因?yàn)橐{(diào)用preNode的方法辉饱,所以要進(jìn)行判空操作搬男。
  • 當(dāng)前結(jié)點(diǎn)訪問(wèn)完,即將訪問(wèn)下一個(gè)結(jié)點(diǎn)了彭沼,于是當(dāng)前結(jié)點(diǎn)也就成了上一個(gè)結(jié)點(diǎn)preNode缔逛。

這里設(shè)置某結(jié)點(diǎn)的后繼之所以使用preNode,理由很簡(jiǎn)單姓惑,當(dāng)前結(jié)點(diǎn)的后繼還沒(méi)訪問(wèn)到呢褐奴,只能用已經(jīng)訪問(wèn)過(guò)的前一個(gè)結(jié)點(diǎn),它的后繼可能是當(dāng)前結(jié)點(diǎn)挺益。

再來(lái)看中序遍歷歉糜。

public void inOrderTraversal(Node<Item> node) {
    // 不斷深入左子樹,直到遇見第一個(gè)線索
    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;
    }
}

先是從根結(jié)點(diǎn)開始按照左子樹深入乘寒,直到遇見第一個(gè)左孩子是線索的結(jié)點(diǎn)望众,緊接著就打印它,這次打印的其實(shí)是鏈表頭伞辛。接下來(lái)看它的右孩子是不是后繼烂翰,如果是就繼續(xù)打印蚤氏;直到右孩子不是線索甘耿,此時(shí)轉(zhuǎn)到右子樹,開始和根結(jié)點(diǎn)一樣的循環(huán)...最后一個(gè)while中需要判斷node.rchild不為空竿滨,如果為空佳恬,我們打印出來(lái)就是null,這不是我們想要看得結(jié)果于游。

前序線索化

把設(shè)置標(biāo)志那一團(tuán)代碼放到遞歸左右子樹之前毁葱,就成了前序線索化。特別注意贰剥,在遞歸前判斷了是否為線索結(jié)點(diǎn)倾剿。因?yàn)榍靶蚓€索化中,遞歸發(fā)生在設(shè)置標(biāo)志位之后蚌成,所以node.lchildnode.rchild可能已經(jīng)被設(shè)置了標(biāo)志前痘。若還遞歸就會(huì)打亂了已設(shè)置好的標(biāo)志位,而且還會(huì)發(fā)生StackOverflow担忧。而中序線索化中無(wú)需判斷是因?yàn)榍鄣蓿f歸左子樹發(fā)生在設(shè)置左孩子的線索之前,而右孩子的線索的設(shè)置是針對(duì)上一個(gè)結(jié)點(diǎn)的瓶盛,當(dāng)前結(jié)點(diǎn)的右孩子并沒(méi)有線索化最欠,所以對(duì)當(dāng)前結(jié)點(diǎn)的右子樹node.rchild的遞歸并沒(méi)有影響坡锡。

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

接著看前序遍歷的。還是根結(jié)點(diǎn)開始窒所,深入左子樹鹉勒,只要沒(méi)遇到左孩子是線索的結(jié)點(diǎn),就立即打印吵取。然后跳出while循環(huán)打印第一個(gè)左孩子是線索的結(jié)點(diǎn)禽额。然后轉(zhuǎn)向其后繼或者是右子樹,繼續(xù)大循壞...

如果我們經(jīng)常需要遍歷二叉樹皮官,并且要知道某結(jié)點(diǎn)的前驅(qū)后繼信息脯倒,那么使用線索二叉樹是個(gè)不錯(cuò)的選擇。特別注意一點(diǎn)捺氢,線索化只能執(zhí)行一次藻丢,已經(jīng)設(shè)置好的標(biāo)志信息就不要再設(shè)置第二次了(除非清空標(biāo)志位),否則會(huì)導(dǎo)致標(biāo)志位的混亂摄乒,導(dǎo)致遍歷失敗悠反。


by @sunhaiyu

2017.9.14

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市馍佑,隨后出現(xiàn)的幾起案子斋否,更是在濱河造成了極大的恐慌,老刑警劉巖拭荤,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件茵臭,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡舅世,警方通過(guò)查閱死者的電腦和手機(jī)旦委,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)雏亚,“玉大人缨硝,你說(shuō)我怎么就攤上這事∑滥” “怎么了追葡?”我有些...
    開封第一講書人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)奕短。 經(jīng)常有香客問(wèn)我宜肉,道長(zhǎng),這世上最難降的妖魔是什么翎碑? 我笑而不...
    開封第一講書人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任谬返,我火速辦了婚禮,結(jié)果婚禮上日杈,老公的妹妹穿的比我還像新娘遣铝。我一直安慰自己佑刷,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開白布酿炸。 她就那樣靜靜地躺著瘫絮,像睡著了一般。 火紅的嫁衣襯著肌膚如雪填硕。 梳的紋絲不亂的頭發(fā)上麦萤,一...
    開封第一講書人閱讀 49,749評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音扁眯,去河邊找鬼壮莹。 笑死,一個(gè)胖子當(dāng)著我的面吹牛姻檀,可吹牛的內(nèi)容都是我干的命满。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼绣版,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼胶台!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起僵娃,我...
    開封第一講書人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤概作,失蹤者是張志新(化名)和其女友劉穎腋妙,沒(méi)想到半個(gè)月后默怨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡骤素,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年匙睹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片济竹。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡痕檬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出送浊,到底是詐尸還是另有隱情梦谜,我是刑警寧澤,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布袭景,位于F島的核電站唁桩,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏耸棒。R本人自食惡果不足惜荒澡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望与殃。 院中可真熱鬧单山,春花似錦碍现、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至悴晰,卻和暖如春辩棒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背膨疏。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工一睁, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人佃却。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓者吁,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親饲帅。 傳聞我的和親對(duì)象是個(gè)殘疾皇子复凳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 1.樹(Tree): 樹是 n(n>=0) 個(gè)結(jié)點(diǎn)的有限集。當(dāng) n=0 時(shí)稱為空樹灶泵。在任意一顆非空樹中:有且僅有一...
    ql2012jz閱讀 994評(píng)論 0 3
  • 四赦邻、樹與二叉樹 1. 二叉樹的順序存儲(chǔ)結(jié)構(gòu) 二叉樹的順序存儲(chǔ)就是用數(shù)組存儲(chǔ)二叉樹髓棋。二叉樹的每個(gè)結(jié)點(diǎn)在順序存儲(chǔ)中都有...
    MinoyJet閱讀 1,512評(píng)論 0 7
  • 原鏈接:理解線索二叉樹|CloudWong 線索二叉樹原理 遍歷二叉樹的其實(shí)就是以一定規(guī)則將二叉樹中的結(jié)點(diǎn)排列成一...
    簡(jiǎn)Cloud閱讀 8,380評(píng)論 1 16
  • 前面講到的順序表按声、棧和隊(duì)列都是一對(duì)一的線性結(jié)構(gòu),這節(jié)講一對(duì)多的線性結(jié)構(gòu)——樹恬吕∏┰颍「一對(duì)多」就是指一個(gè)元素只能有一個(gè)前...
    Alent閱讀 2,229評(píng)論 1 28
  • 概念 樹是什么 樹(Tree)是n(n>=0)個(gè)結(jié)點(diǎn)的有限集。 n = 0的樹是空樹铐料。 在任意一棵非空樹中: 有且...
    剛剛悟道閱讀 5,034評(píng)論 1 16