二叉樹(shù)的遍歷

樹(shù)是一種經(jīng)常用到的結(jié)構(gòu),用來(lái)模擬具有樹(shù)狀結(jié)構(gòu)性質(zhì)的數(shù)據(jù)集合

樹(shù)的每一個(gè)節(jié)點(diǎn)有一個(gè)和一個(gè)包含所有子節(jié)點(diǎn)的列表明未,從圖的觀點(diǎn)來(lái)看槽华,樹(shù)也可視為一個(gè)擁有N 個(gè)節(jié)點(diǎn)N-1 條邊的一個(gè)有向無(wú)環(huán)圖

二叉樹(shù)是一種更為典型的樹(shù)狀結(jié)構(gòu)趟妥。如它名字所描述的那樣硼莽,二叉樹(shù)是每個(gè)節(jié)點(diǎn)最多有兩個(gè)子樹(shù)的樹(shù)結(jié)構(gòu),通常子樹(shù)被稱作“左子樹(shù)”“右子樹(shù)”煮纵。

樹(shù)的遍歷

  • 前序遍歷

首先訪問(wèn)根節(jié)點(diǎn),然后遍歷左子樹(shù)偏螺,最后遍歷右子樹(shù)

      F
    /   \
   B      G
  / \       \
 A   D       I
    / \     / 
  C    E   H
//遍歷結(jié)構(gòu)是 
F行疏、B、A套像、D酿联、C、E夺巩、G贞让、I、H
  • 中序遍歷

先遍歷左子樹(shù)柳譬,然后訪問(wèn)根節(jié)點(diǎn)喳张,最后訪問(wèn)右子樹(shù)

      F
    /   \
   B      G
  / \       \
 A   D       I
    / \     / 
  C    E   H
//遍歷結(jié)構(gòu)是 
A、B美澳、C销部、D、E制跟、F舅桩、G、H雨膨、I
  • 后序遍歷

先遍歷左子樹(shù)擂涛,然后遍歷右子樹(shù),最后訪問(wèn)根節(jié)點(diǎn)

      F
    /   \
   B      G
  / \       \
 A   D       I
    / \     / 
  C    E   H
//遍歷結(jié)構(gòu)是 
A聊记、C撒妈、E、D排监、B踩身、H、I社露、G挟阻、F

總結(jié)

所謂前中后都是根據(jù)根節(jié)點(diǎn)的位置來(lái)命名的。后序在數(shù)學(xué)表達(dá)式中廣泛使用,編寫(xiě)程序來(lái)解析后綴法更加容易附鸽。

這里舉一個(gè)例子:



對(duì)于這個(gè)圖脱拼,我們使用中序遍歷很容易能找出表達(dá)式:4x(7-2)+5

如果你想對(duì)這棵樹(shù)進(jìn)行后序遍歷,使用棧來(lái)處理表達(dá)式會(huì)變得更加容易坷备。 每遇到一個(gè)操作符熄浓,就可以從棧中彈出棧頂?shù)膬蓚€(gè)元素,計(jì)算并將結(jié)果返回到棧中省撑。

遍歷

我們有三種常用的遍歷方法赌蔑、遞歸、迭代竟秫、morris娃惯,我們以給定的節(jié)點(diǎn)類如下,分別介紹三種遍歷方法

 public class TreeNode {
      int val;
      TreeNode left;
      TreeNode right;
      TreeNode() {}
      TreeNode(int val) { this.val = val; }
      TreeNode(int val, TreeNode left, TreeNode right) {
          this.val = val;
          this.left = left;
          this.right = right;
      }
  }
  • 遞歸

    //遞歸邏輯是最簡(jiǎn)單的肥败,我們只需要把輸出加在不同的位置
    public static void order(TreeNode tree) {
        if (tree == null)
            return;
          //前序?qū)懛ㄖ呵常∠旅娲a注釋
        // System.out.printf(tree.val + "");
          //然后輸出左節(jié)點(diǎn)
        preOrder(tree.left);
          //前序?qū)懛ǎ∠旅娲a注釋
        // System.out.printf(tree.val + "");
          //然后輸出右節(jié)點(diǎn)
        preOrder(tree.right);
        //后序?qū)懛裕∠旅娲a注釋
        // System.out.printf(tree.val + "");
    }
    
  • 迭代

    本質(zhì)上是在模擬遞歸皿哨,因?yàn)樵谶f歸的過(guò)程中使用了系統(tǒng)棧,所以在迭代的解法中常用 Stack 來(lái)模擬系統(tǒng)棧纽谒。

    • 前序

      首先我們應(yīng)該創(chuàng)建一個(gè)Stack 用來(lái)存放節(jié)點(diǎn)证膨,首先我們想要打印根節(jié)點(diǎn)的數(shù)據(jù),此時(shí) Stack里面的內(nèi)容為空鼓黔,所以我們優(yōu)先將頭結(jié)點(diǎn)加入Stack椎例,然后打印。

      之后我們應(yīng)該先打印左子樹(shù)请祖,然后右子樹(shù)订歪。所以先加入 Stack 的就是右子樹(shù),然后左子樹(shù)肆捕。
      此時(shí)你能得到的流程如下:

```java
public static void preOrderIteration(TreeNode head) {
    if (head == null) {
        return;
    }
    Stack<TreeNode> stack = new Stack<>();
    stack.push(head);
    while (!stack.isEmpty()) {
        TreeNode node = stack.pop();
        System.out.print(node.value + " ");
        if (node.right != null) {
            stack.push(node.right);
        }
        if (node.left != null) {
            stack.push(node.left);
        }
    }
}
```
  • 中序

    1. 同理創(chuàng)建一個(gè) Stack刷晋,然后按 左 中 右的順序輸出節(jié)點(diǎn)。
    2. 盡可能的將這個(gè)節(jié)點(diǎn)的左子樹(shù)壓入 Stack慎陵,此時(shí)棧頂?shù)脑厥亲钭髠?cè)的元素眼虱,其目的是找到一個(gè)最小單位的子樹(shù)(也就是最左側(cè)的一個(gè)節(jié)點(diǎn)),并且在尋找的過(guò)程中記錄了來(lái)源席纽,才能返回上層,同時(shí)在返回上層的時(shí)候已經(jīng)處理完畢左子樹(shù)了捏悬。。
    3. 當(dāng)處理完最小單位的子樹(shù)時(shí)润梯,返回到上層處理了中間節(jié)點(diǎn)过牙。(如果把整個(gè)左中右的遍歷都理解成子樹(shù)的話甥厦,就是處理完 左子樹(shù)->中間(就是一個(gè)節(jié)點(diǎn))->右子樹(shù))
    4. 如果有右節(jié)點(diǎn),其也要進(jìn)行中序遍歷寇钉。
當(dāng)整個(gè)左子樹(shù)退棧的時(shí)候這個(gè)時(shí)候輸出了該子樹(shù)的根節(jié)點(diǎn) 2刀疙,之后輸出中間節(jié)點(diǎn) 1。然后處理根節(jié)點(diǎn)為3右子樹(shù)扫倡。

```java
public static void inOrderIteration(TreeNode head) {
    if (head == null) {
        return;
    }
    TreeNode cur = head;
    Stack<TreeNode> stack = new Stack<>();
    while (!stack.isEmpty() || cur != null) {
        while (cur != null) {
            stack.push(cur);
            cur = cur.left;
        }
        TreeNode node = stack.pop();
        System.out.print(node.value + " ");
        if (node.right != null) {
            cur = node.right;
        }
    }
}
```
  • 后序

    //1 前序遍歷的過(guò)程 是 中左右谦秧。
    //2 將其轉(zhuǎn)化成 中右左。也就是壓棧的過(guò)程中優(yōu)先壓入左子樹(shù)撵溃,在壓入右子樹(shù)疚鲤。
    //3 然后將這個(gè)結(jié)果返回來(lái),這里是利用棧的先進(jìn)后出倒序打印缘挑。
    public static void postOrderIteration(TreeNode head) {
            if (head == null) {
                return;
            }
            Stack<TreeNode> stack1 = new Stack<>();
            Stack<TreeNode> stack2 = new Stack<>();
            stack1.push(head);
            while (!stack1.isEmpty()) {
                TreeNode node = stack1.pop();
                stack2.push(node);
                if (node.left != null) {
                    stack1.push(node.left);
                }
                if (node.right != null) {
                    stack1.push(node.right);
                }
            }
            while (!stack2.isEmpty()) {
                System.out.print(stack2.pop().value + " ");
            }
        }
    
    //1 用一個(gè)指針 cur 標(biāo)記當(dāng)前退出的節(jié)點(diǎn)是什么集歇。
    //2 后序遍歷的過(guò)程中在遍歷完左子樹(shù)跟右子樹(shù) cur 都會(huì)回到根結(jié)點(diǎn)。所以當(dāng)前不管是從左子樹(shù)還是右子樹(shù)回到根結(jié)點(diǎn)都不應(yīng)該再操作了卖哎,應(yīng)該退回上層。
    //3 如果是從右邊再返回根結(jié)點(diǎn)删性,應(yīng)該回到上層亏娜。
    
    public static void postOrderIteration2(TreeNode head) {
        if (head == null) {
            return;
        }
        TreeNode cur = head;
        Stack<TreeNode> stack = new Stack<>();
        stack.push(head);
        while (!stack.isEmpty()) {
            TreeNode peek = stack.peek();
            if (peek.left != null && peek.left != cur && peek.right != cur) {
                stack.push(peek.left);
            } else if (peek.right != null && peek.right != cur) {
                stack.push(peek.right);
            } else {
                System.out.print(stack.pop().val + " ");
                cur = peek;
            }
        }
    }
    
  • Morris解法

    Morris 遍歷使用二叉樹(shù)節(jié)點(diǎn)中大量指向 null 的指針,由 Joseph Morris 于 1979 年發(fā)明蹬挺。
    時(shí)間復(fù)雜度:O(n)
    額外空間復(fù)雜度:O(1)

    在你閱讀以下代碼之前维贺,在這邊先講解一下 Morris 的通用解法過(guò)程。

Morris 的整體思路就是將 以某個(gè)根結(jié)點(diǎn)開(kāi)始巴帮,找到它左子樹(shù)的最右側(cè)節(jié)點(diǎn)之后與這個(gè)根結(jié)點(diǎn)進(jìn)行連接
我們可以從 圖2 看到溯泣,如果這么連接之后,cur 這個(gè)指針是可以完整的從一個(gè)節(jié)點(diǎn)順著下一個(gè)節(jié)點(diǎn)遍歷榕茧,將整棵樹(shù)遍歷完畢垃沦,直到 7 這個(gè)節(jié)點(diǎn)右側(cè)沒(méi)有指向。


public static void preOrderMorris(TreeNode head) {
  if (head == null) {
      return;
  }
  TreeNode cur1 = head;//當(dāng)前開(kāi)始遍歷的節(jié)點(diǎn)
  TreeNode cur2 = null;//記錄當(dāng)前結(jié)點(diǎn)的左子樹(shù)
  while (cur1 != null) {
      cur2 = cur1.left;
      if (cur2 != null) {
          while (cur2.right != null && cur2.right != cur1) {//找到當(dāng)前左子樹(shù)的最右側(cè)節(jié)點(diǎn)用押,且這個(gè)節(jié)點(diǎn)應(yīng)該在指向根結(jié)點(diǎn)之前肢簿,否則整個(gè)節(jié)點(diǎn)又回到了根結(jié)點(diǎn)。
              cur2 = cur2.right;
          }
          if (cur2.right == null) {//這個(gè)時(shí)候如果最右側(cè)這個(gè)節(jié)點(diǎn)的右指針沒(méi)有指向根結(jié)點(diǎn)蜻拨,創(chuàng)建連接然后往下一個(gè)左子樹(shù)的根結(jié)點(diǎn)進(jìn)行連接操作池充。
              cur2.right = cur1;
              cur1 = cur1.left;
              continue;
          } else {//當(dāng)左子樹(shù)的最右側(cè)節(jié)點(diǎn)有指向根結(jié)點(diǎn),此時(shí)說(shuō)明我們已經(jīng)回到了根結(jié)點(diǎn)并重復(fù)了之前的操作缎讼,同時(shí)在回到根結(jié)點(diǎn)的時(shí)候我們應(yīng)該已經(jīng)處理完 左子樹(shù)的最右側(cè)節(jié)點(diǎn) 了收夸,把路斷開(kāi)。
              cur2.right = null;
          }
      } 
      cur1 = cur1.right;//一直往右邊走血崭,參考圖
  }
}
  • 前序遍歷

    1. 在某個(gè)根結(jié)點(diǎn)創(chuàng)建連線的時(shí)候打印卧惜。因?yàn)槲覀兪琼樦筮叺母?jié)點(diǎn)來(lái)創(chuàng)建連線厘灼,且創(chuàng)建的過(guò)程只有一次。
    2. 打印某些自身無(wú)法創(chuàng)建連線的節(jié)點(diǎn)序苏,也就是葉子節(jié)點(diǎn)手幢。
    public static void preOrderMorris(TreeNode head) {
        if (head == null) {
            return;
        }
        TreeNode cur1 = head;
        TreeNode cur2 = null;
        while (cur1 != null) {
            cur2 = cur1.left;
            if (cur2 != null) {
                while (cur2.right != null && cur2.right != cur1) {
                    cur2 = cur2.right;
                }
                if (cur2.right == null) {
                    cur2.right = cur1;
                    System.out.print(cur1.value + " ");
                    cur1 = cur1.left;
                    continue;
                } else {
                    cur2.right = null;
                }
            } else {
                System.out.print(cur1.value + " ");
            }
            cur1 = cur1.right;
        }
    }
    
  • 中序遍歷

    從最左側(cè)開(kāi)始順著右節(jié)點(diǎn)打印。也就是在將 cu1 切換到上層節(jié)點(diǎn)的時(shí)候忱详。

    public static void inOrderMorris(TreeNode head) {
        if (head == null) {
            return;
        }
        TreeNode cur1 = head;
        TreeNode cur2 = null;
        while (cur1 != null) {
            cur2 = cur1.left;
            //構(gòu)建連接線
            if (cur2 != null) {
                while (cur2.right != null && cur2.right != cur1) {
                    cur2 = cur2.right;
                }
                if (cur2.right == null) {
                    cur2.right = cur1;
                    cur1 = cur1.left;
                    continue;
                } else {
                    cur2.right = null;
                }
            }
            System.out.print(cur1.value + " ");
            cur1 = cur1.right;
        }
    }
    
  • 后序遍歷

當(dāng)我們到達(dá)最左側(cè)围来,也就是左邊連線已經(jīng)創(chuàng)建完畢了。
打印 4
打印 5 2
打印 6
打印 7 3 1
我們將一個(gè)節(jié)點(diǎn)的連續(xù)右節(jié)點(diǎn)當(dāng)成一個(gè)單鏈表來(lái)看待匈睁。
當(dāng)我們返回上層之后监透,也就是將連線斷開(kāi)的時(shí)候,打印下層的單鏈表航唆。
比如返回到≌吐2,此時(shí)打印∨锤啤4
比如返回到》嗬恰1,此時(shí)打印∪伟丁5≡匍2
比如返回到 3享潜,此時(shí)打印±浮6
那么我們只需要將這個(gè)單鏈表逆序打印就行了,下文也給出了 單鏈表逆序代碼
這里不應(yīng)該打印當(dāng)前層剑按,而是下一層疾就,否則根結(jié)點(diǎn)會(huì)先與右邊打印。

public static void postOrderMorris(TreeNode head) {
  if (head == null) {
      return;
  }
  TreeNode cur1 = head;//遍歷樹(shù)的指針變量
  TreeNode cur2 = null;//當(dāng)前子樹(shù)的最右節(jié)點(diǎn)
  while (cur1 != null) {
      cur2 = cur1.left;
      if (cur2 != null) {
          while (cur2.right != null && cur2.right != cur1) {
              cur2 = cur2.right;
          }
          if (cur2.right == null) {
              cur2.right = cur1;
              cur1 = cur1.left;
              continue;
          } else {
              cur2.right = null;
              postMorrisPrint(cur1.left);
          }
      }
      cur1 = cur1.right;
  }
  postMorrisPrint(head);
}
//打印函數(shù)
public static void postMorrisPrint(TreeNode head) {
  TreeNode reverseList = postMorrisReverseList(head);
  TreeNode cur = reverseList;
  while (cur != null) {
      System.out.print(cur.value + " ");
      cur = cur.right;
  }
  postMorrisReverseList(reverseList);
}
//翻轉(zhuǎn)單鏈表
public static TreeNode postMorrisReverseList(TreeNode head) {
  TreeNode cur = head;
  TreeNode pre = null;
  while (cur != null) {
      TreeNode next = cur.right;
      cur.right = pre;
      pre = cur;
      cur = next;
  }
  return pre;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末艺蝴,一起剝皮案震驚了整個(gè)濱河市猬腰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌猜敢,老刑警劉巖漆诽,帶你破解...
    沈念sama閱讀 221,695評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異锣枝,居然都是意外死亡厢拭,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門撇叁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)供鸠,“玉大人,你說(shuō)我怎么就攤上這事陨闹±阄妫” “怎么了薄坏?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,130評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)寨闹。 經(jīng)常有香客問(wèn)我胶坠,道長(zhǎng),這世上最難降的妖魔是什么繁堡? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,648評(píng)論 1 297
  • 正文 為了忘掉前任沈善,我火速辦了婚禮,結(jié)果婚禮上椭蹄,老公的妹妹穿的比我還像新娘闻牡。我一直安慰自己,他們只是感情好绳矩,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布罩润。 她就那樣靜靜地躺著,像睡著了一般翼馆。 火紅的嫁衣襯著肌膚如雪割以。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,268評(píng)論 1 309
  • 那天应媚,我揣著相機(jī)與錄音严沥,去河邊找鬼。 笑死珍特,一個(gè)胖子當(dāng)著我的面吹牛祝峻,可吹牛的內(nèi)容都是我干的魔吐。 我是一名探鬼主播扎筒,決...
    沈念sama閱讀 40,835評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼酬姆!你這毒婦竟也來(lái)了嗜桌?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,740評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤辞色,失蹤者是張志新(化名)和其女友劉穎骨宠,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體相满,經(jīng)...
    沈念sama閱讀 46,286評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡层亿,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了立美。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片匿又。...
    茶點(diǎn)故事閱讀 40,505評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖建蹄,靈堂內(nèi)的尸體忽然破棺而出碌更,到底是詐尸還是另有隱情裕偿,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布痛单,位于F島的核電站嘿棘,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏旭绒。R本人自食惡果不足惜鸟妙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望快压。 院中可真熱鬧圆仔,春花似錦、人聲如沸蔫劣。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,357評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)脉幢。三九已至歪沃,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間嫌松,已是汗流浹背沪曙。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,466評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留萎羔,地道東北人液走。 一個(gè)月前我還...
    沈念sama閱讀 48,921評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像贾陷,于是被迫代替她去往敵國(guó)和親缘眶。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評(píng)論 2 359

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