【經典結構】二叉樹

二叉樹

1.基本概念

二叉樹是每一個節點最多有兩個子樹的樹結構,度多是0,1,2;java

完成二叉樹:從左到右依次填滿;
滿二叉樹:除了葉子節點,全部節點都有兩個孩子,而且全部葉子節點在同一層;node

2.性質

1.徹底二叉樹除了最後一層外,下一層節點個數是上一層兩倍,
若是一顆徹底二叉樹的節點總數是n,那麼葉子節點個數爲n/2(n爲偶數)或(n+1)/2(n爲奇數);git

3.遞歸在二叉樹中的應用

寫遞歸算法的關鍵就是明確函數的定義是什麼,而後相信這個定義,利用這個定義推道出最終結果,毫不要跳入遞歸的細節 (人的小腦殼才能堆幾層棧)算法

寫樹相關的算法,簡單的就是說,就是搞清楚當前root節點「該作什麼,何時作」這兩點,而後根據函數定義遞歸調用子節點。框架

  • 該作什麼:就是咱們的root節點應該作什麼。
    • 作點什麼可以提供給下面的子樹(先序)
    • 能從下面的子樹上得到什麼信息而後利用(後序)
  • 何時作:剛纔寫的代碼應該放在前序、中序仍是後序的代碼位置上。

把題目的要求細化,搞清楚根節點應該作什麼,而後剩下的事情拋給前/中/後序的遍歷框架就好了,難點在於如何經過題目的要求思考出每個節點須要作什麼。函數

4.遍歷

4.1 概念

  • 前序遍歷:根節點 -> 左子樹 -> 右子樹;
  • 中序遍歷:左子樹 -> 根節點 -> 右子樹;
  • 後序遍歷:左子樹 -> 右子樹 -> 根節點;

4.2 遞歸實現

遞歸的時候不要太在乎實現的細節,其本質上是經過棧來實現的,每次在方法裏本身調本身,就把新調用的本身壓棧,是一個有去有回的過程。
對於遞歸,關鍵就是要清楚函數的功能,何時停下來。
對於前序遍歷,想先打印根節點,再左再右;那就先輸出,再去遞歸調用,傳入當前節點的左子樹,左子樹一樣打印它的根,傳入根的左子樹,知道整個左子樹處理完了,再去處理右子樹;post

class BinaryTreeTraverse<T>{

     /**
     * 前序遍歷(遞歸實現);
     */
    public static void preOrderByRecursion(TreeNode node){
        if (node == null) return; //遞歸終止條件;
        System.out.println(node.value); //獲取根節點的值;
        preOrderByRecursion(node.left); //左子樹的根節點;
        preOrderByRecursion(node.right);//右子樹的根節點;
    }
     /**
     * 中序遍歷(遞歸實現)
     */
    public static void inOrderByRecursion(TreeNode node){
        if (node == null) return; //遞歸終止;
        inOrderByRecursion(node.left); //先左子樹;
        System.out.println(node.value); //再根節點;
        inOrderByRecursion(node.right); //再右節點;
    }
     /**
     * 後序遍歷(遞歸實現)
     */
    public static void postOrderByRecursion(TreeNode node){
        if (node == null) return;
        postOrderByRecursion(node.left);
        postOrderByRecursion(node.right);
        System.out.println(node.value);
    }

時間複雜度:0(N),每一個節點遍歷N次;
空間複雜度:O(N),遞歸過程當中棧的開銷;ui

4.3 迭代實現

前序遍歷
前序遍歷就是咱們來手動實如今遞歸過程當中的棧。
想要實現先左再右,那壓棧的時候右先入棧,左再入棧。
以下圖所示;
image
規則:
壓入根節點;
1.彈出就打印;
2.若有右孩子,壓入右;
3.若有左孩子,壓入左;
重複1.2.3
思考一下這個過程;其實就是至關於兩個孩子來替換棧裏的根節點,這就很符合前序的定義,根先走,左子樹幹到了最頂部,要是我左子樹還有左孩子,那我也走,兩個孩子來替我,每次都是我先走,我左孩子這邊整個都完事了,再來我右孩子,由於我右孩子被壓在最下面。this

public static void preOrder(TreeNode node){
    Stack<TreeNode> stack = new Stack<>();
    if (node != null){
        stack.push(node); //根節點入棧;
    }
    while (!stack.isEmpty()){
        TreeNode top = stack.pop(); //彈出就打印;
        System.out.println(top.value);
        if (top.right != null) stack.push(top.right); //依次入棧右節點和左節點;
        if (top.left != null) stack.push(top.left);
    }
}

中序遍歷
規則:
1.整條左邊界依次入棧;
2.條件1執行不了了,彈出就打印;
3.來到彈出節點的右子樹上,繼續執行條件1;(右樹爲空,執行2.彈出就打印;右樹有節點,執行1.壓棧;
說明:將整個樹全用左邊界去看,都是先處理了左邊界,將左邊界分解成了左頭,頭先入,左再入,而後弄不動了,彈出,而後看其右節點,再把右節點裏的左依次進去;就這樣往返;
以下如所示;.net

image

public static void inOrder(TreeNode node){
    Stack<TreeNode> stack = new Stack<>();
    while (!stack.isEmpty() || node != null){
        if (node != null){ //條件1;能往左走就往左走;
            stack.push(node);
            node = node.left;
        }else {
            TreeNode top = stack.pop(); //條件2;彈出打印;
            System.out.println(top.value);
            node = top.right; //條件3;來彈出節點右樹上,繼續1;
        }
    }
}

後序遍歷
後序遍歷能夠用前序遍從來解決,想一下前序遍歷:根左右,咱們先壓右樹再壓左樹。怎麼實現根右左呢,能夠先壓左樹再壓右樹嘛,而後反過來不就是左右根了嗎?(反過來用棧來實現,棧一個很大的做用就是實現逆序)

public static void postOrder(TreeNode node){
    Stack<TreeNode> stackA = new Stack<>();
    Stack<TreeNode> stackB = new Stack<>();
    if (node != null){
        stackA.push(node);
    }
    while (!stackA.isEmpty()){
        TreeNode top = stackA.pop();
        stackB.push(top);  //棧A彈出的進入棧B;先實現根右左,B倒序實現左右根;
        if (top.left != null) stackA.push(top.left);
        if (top.right != null) stackA.push(top.right);
    }
    while (!stackB.isEmpty()){
        System.out.println(stackB.pop().value);
    }
}

4.4 Morris實現

二叉樹的結構中只有父節點指向孩子節點,孩子節點不能向上指,因此須要棧。
而Morris遍歷的實質就是讓下層節點也能指向上層,怎麼辦呢,一個節點有兩個指針,左和右,若是這兩個指針都有指向具體節點了那指定用不上了。
可是二叉樹但是有不少空閒指針啊,好比說全部的葉子節點,它們的指針就都指向null,因此能夠利用其right指針指向上層。
這樣把下層往上層創建鏈接之後,cur指針就能夠完整的順着一個鏈條遍歷完整個樹。
由於不用堆棧,因此其空間複雜度變爲O(1);
以下圖所示:

image

cur指針走的順序:1 2 4 2 5 1 3 6 3 7;
核心: 以某個根節點開始,找到其左子樹的最右節點(必然是個葉子節點),而後利用其right指針指向根節點(創建從下到上的鏈接)

  • 到達兩次的是有左子樹的節點;
  • 到達一次的是沒有左子樹的節點;

原則:

  • 1.若是cur無左孩子,cur向右移動(cur=cur.right)
  • 2.若是cur有左孩子,找到cur左子樹上最右的節點,記爲mostRightNode;
    • 1.若是mostRightNode的right指針指向空,讓其指向cur,cur向左移動(cur=cur.left)
    • 2.若是mostRightNode的right指針指向cur,讓其指向空,cur向右移動(cur=cur.right)

(迭代法的中序遍歷咱們能夠將整個樹所有分紅左邊界去看,如上面的圖,其實在morris遍歷裏咱們能夠將整個樹所有分紅右邊界來看)

public static void morris(TreeNode node){
    if(node == null){
        return;
    }
    TreeNode cur = node;
    TreeNode mostRightNode = null; //記錄cur左子樹的最右節點;
    while(cur != null){
        mostRightNode = cur.left;
        if(mostRightNode != null){ //cur有左子樹,就證實有下一層;
            //找到cur左子樹的最右節點(找到cur下一層右邊界的最後一個)
            while(mostRight.right != null && mostRightNode != cur){
                //1.最右節點爲空說明到頭了,找到了;
                //2.最右節點指向上層說明已經處理過了,來過了;
                mostRightNode = mostRightNode.right;
            }
            //走到這裏證實跳出上面循環,無非兩個緣由:
            //1.右邊沒了;2.右邊指向上層了(以前就處理過了);
            if(mostRightNode.right == null){
                mostRightNode.right = cur; //創建從最右節點到cur的鏈接;
                cur = cur.left; //處理下一個節點;
                continue;
            }else{
                //能到這裏說明已經創建了最右節點到cur的鏈接;
                //也說明cur指的這個節點是第二次到了,斷開鏈接;
                mostRightNode.right = null;
            }
        }
        //cur右移的狀況:
        //1.cur沒有左子樹了(天然要開始處理右子樹)
        //2.cur有左子樹,可是cur左子樹最右節點已經指向cur了(執行完上面else斷開後,cur左邊已經徹底處理好了,開始右移。)
        cur = cur.right;
    }
}

前序遍歷

1.對於cur只達到一次的節點(沒有左子樹),cur達到就打印;
2.對於cur到達兩次的節點(有左子樹),到達第一次時打印;

public static void preOrderMorris(TreeNode node){
    if (node == null){
        return;
    }
    TreeNode cur = node;
    TreeNode mostRightNode = null;
    while (cur != null){
        mostRightNode = cur.left;
        if (mostRightNode != null){ //到達兩次的節點;
            //找到cur左子樹的最右節點;
            while (mostRightNode.right != null && mostRightNode.right != cur){
                mostRightNode = mostRightNode.right;
            }
            if (mostRightNode.right == null){
                mostRightNode.right = cur;  //指向上層cur;
                System.out.println(cur.value); //第一次到的時候打印;
                cur = cur.left;
                continue;
            }else{
                mostRightNode.right = null; //第二次到時不打印;
            }
        }else {
            System.out.println(cur.value); //只到達一次的節點;
        }
        cur = cur.right;
    }
}

中序遍歷

1.對於cur只達到一次的節點(沒有左子樹),cur達到就打印;
2.對於cur到達兩次的節點(有左子樹),到達第二次時打印;

public static void inOrderMorris(TreeNode node){
    if (node == null){
        return;
    }
    TreeNode cur = node;
    TreeNode mostRightNode = null;
    while (cur != null){
        mostRightNode = cur.left;
        if (mostRightNode != null){ //有左子樹,到達兩次的節點;
            while (mostRightNode.right != null && mostRightNode.right != cur){
                mostRightNode = mostRightNode.right;
            }
            if (mostRightNode == null){
                mostRightNode.right = cur; //第一次不打印;
                cur = cur.left;
                continue;
            }else {
                System.out.println(cur.value); //第二次遇到時打印;
                mostRightNode.right = null;
            }
        }else {
            System.out.println(cur.value); //只到達一次的節點,遇到就打印;
        }
        cur = cur.right;
    }
}

後序遍歷

後序遍歷比前面兩個要複雜一點;
將一個節點的連續右節點當成是一個單鏈表看,以下圖所示:

image

當咱們到達最左側,也就是左邊連線已經建立完畢了。
打印 4
打印 5 2
打印 6
打印 7 3 1
咱們將一個節點的連續右節點當成一個單鏈表來看待。
當咱們返回上層以後,也就是將連線斷開的時候,打印下層的單鏈表
好比返回到 2,此時打印 4
好比返回到 1,此時打印 5 2
好比返回到 3,此時打印 6
最後別忘記頭節點那一串,即1 3 7
那麼咱們只須要將這個單鏈表逆序打印就好了。
這裏不該該打印當前層,而是下一層,不然根結點會先與右邊打印。

public static void postOrderMorris(TreeNode node){
    if (node == null){
        return;
    }
    TreeNode cur = node;
    TreeNode mostRightNode = null;
    while (node != null){
        mostRightNode = cur.left;
        if (mostRightNode != null){
            while (mostRightNode.right != null && mostRightNode.right != cur){
                mostRightNode = mostRightNode.right;
            }
            if (mostRightNode.right == null){
                mostRightNode.right = cur;
                cur = cur.left;
                continue;
            }else { //能到這裏的都是達到過兩次的,也就是是有左孩子的。
                mostRightNode.right = null; //這時候是已經返回上層以後,斷開了鏈接,因此打印下層的單鏈表;
                postMorrisPrint(cur.left);
            }
        }
        cur = cur.right;
    }
    postMorrisPrint(node); //最後把頭節點那一串右打印一遍;
}
public static void postMorrisPrint(TreeNode node){
    TreeNode reverseList = postMorrisReverseList(node); //反轉鏈表;
    TreeNode cur = reverseList;
    while (cur != null){
        System.out.println(cur.value);
        cur = cur.right;
    }
    postMorrisReverseList(reverseList); //最後再還原;
}
public static TreeNode postMorrisReverseList(TreeNode node){
    TreeNode cur = node;
    TreeNode pre = null;
    while (cur != null){
        TreeNode next = cur.right;
        cur.right = pre;
        pre = cur;
        cur = next;
    }
    return pre;
}

4.5 層次遍歷

層次遍歷顧名思義就是一層一層的遍歷。從上到下,從左到右,那須要藉助什麼結構呢?
能夠採用隊列的結構,利用其先進先出的特性,每一層依次入隊,再依次出隊。
對該層節點進行出隊時,將這個節點的左右節點入隊,這樣當一層全部節點出隊完成後,下一層也入隊完成了。

/**
 * 層次遍歷
 * 藉助隊列的結構,每一層依次入隊,再依次出隊;
 * 對該層節點進行出隊操做時,須要將該節點的左孩子和右孩子入隊;
 */
public static int layerOrder(TreeNode node){
    if (node == null) return 0;
    Queue<TreeNode> queue = new LinkedList<>();
    queue.add(node);
    while (!queue.isEmpty()){
        int size = queue.size(); //當前層的節點數量;
        for (int i = 0; i < size; i++){
            TreeNode front = queue.poll();
            System.out.println(front.value);
            if (front.left != null) queue.add(front.left);
            if (front.right != null) queue.add(front.right);
        }
    }
}

5.深度

二叉樹的最大深度是根節點到最遠葉子結點的距離;

5.1 最大深度

遞歸實現

1.終止條件:在二叉樹爲空的時候,深度爲1;
2.縮小範圍,等價關係:給定一個二叉樹,其深度爲左子樹的深度和右子樹的深度的最大值+1;

/**
 * 求最大深度(遞歸)
 * 最大深度是左子樹和右子樹的最大深度的大的那個+1;
 */
public static int maxDepthByRecursion(TreeNode node){
    if(node == null) return 0;
    int leftDepth = maxDepthByRecursion(node.left);
    int rightDepth = maxDepthByRecursion(node.right);
    return Math.max(leftDepth,rightDepth)+1;
}

非遞歸實現(層次遍歷)
關鍵點:每遍歷一層,則計數器加+1;直到遍歷完成,獲得樹的深度。
採用二叉樹的層次遍歷,來計數總共有多少層,採用隊列的結構,當前層節點出隊,計數器加1,而後把下一層的節點所有入隊,直到隊爲空。

/**
 * 求最大深度(非遞歸)
 * 層次遍歷(BFS)
 * 每遍歷一層,則計數器加+1;直到遍歷完成,獲得樹的深度。
 */
public static int maxDepth(TreeNode node){
    if (node == null) return 0;
    Queue<TreeNode> queue = new LinkedList<>();
    int level = 0; //層數;
    queue.add(node);
    while (!queue.isEmpty()){
        level++;
        int levelNum = queue.size(); //每層的節點數;
        for (int i = 0; i < levelNum; i++){
            TreeNode front = queue.poll(); //當前層出隊,下一層入隊;
            if (front.left != null) queue.add(front.left);
            if (front.right != null) queue.add(front.right);
        }
    }
    return level;
}

5.2 最小深度

二叉樹的深度是根節點到最近葉子節點的距離;
遞歸實現

此題不能像最大深度那樣直接求兩顆子樹的最大而後+1,最大深度能夠是由於取大值不會影響一棵樹爲空的時候。可是取最小就不同了,若是一棵樹爲空,那最小的應該是不爲空的那邊的值,可是還按原來方式就變成了0+1;好比下面這個例子:最小深度應該我2.可是按原來方式寫的話最小深度就會變爲1.因此,在處理每個節點的時候,若是有兩個孩子,那就能夠繼續取小+1,若是隻有一個孩子,那就只能去遞歸它的孩子。
image

/**
 * 求最小深度(遞歸)
 * 注意和求最大深度的區別;
 */
public static int minDepthByRecursion(TreeNode node){
    if (node == null) return 0;
    if (node.right == null && node.left == null) return 1;
    if (node.left == null && node.right != null) return minDepthByRecursion(node.right) + 1;
    if (node.right == null && node.left != null) return minDepthByRecursion(node.left) + 1;
    return Math.min(minDepthByRecursion(node.left), minDepthByRecursion(node.right))+1;
}

非遞歸實現(層次遍歷)
關鍵點:每遍歷一層,則計數器加+1;在遍歷的過程當中,若是出現了沒有葉子節點的節點,那就能夠結束了,就是最小深度
採用二叉樹的層次遍歷,來計數總共有多少層,採用隊列的結構,當前層節點出隊,計數器加1,而後把下一層的節點所有入隊,直到遇到葉子節點或隊爲空。

/**
 * 求最小深度(非遞歸)
 */
public static int minDepth(TreeNode node){
    if (node == null) return 0;
    Queue<TreeNode> queue = new LinkedList<>();
    int level = 0;
    queue.add(node);
    while (!queue.isEmpty()){
        level++;
        int levelnum = queue.size();
        for(int i = 0; i < levelnum; i++){
            TreeNode front = queue.poll();
            if (front.left == null && front.right == null){
                return level; //遇到第一個無葉子節點的時候,該節點的深度爲最小深度;
            }
            if (front.right != null){
                queue.add(front.right);
            }
            if (front.left != null){
                queue.add(front.left);
            }
        }
    }
    return level;
}

6.重構二叉樹

根據二叉樹的前序或後序中的一個再加上中序來還原出整個二叉樹。
注意: 中序是必須有的,由於其能夠明確的把左右子樹分開。
先看下3種遍歷的特色(以下圖):

image

特色

  • 1.前序的第一個節點是root,後序的最後一個節點是root。
  • 2.每種排序的左右子樹分佈都是有規律的。
  • 3.每個子樹又能夠當作是一顆全新的樹,仍然遵循上述規律。

6.1 前序+中序

前序的遍歷順序是根左右,中序的遍歷順序是左中右,

遞歸實現
1.前序的第一個節點是root節點,對應可以找到在中序中的位置。
2.根據中序遍歷的特色,在找到的根前邊序列是左子樹的中序遍歷序,後邊序列是右子樹的中序遍歷。
3.求出左邊序列的個數,好比設爲leftSize,那在前序序列中緊跟着根的leftSize個元素是左子樹的前序序列,後邊的爲右子樹的前序序列。
4.這樣就又得到了兩個子樹的前序遍歷和中序遍歷,開始遞歸。

/**
 * 根據前序遍歷和中序遍歷構造二叉樹;
 */
public static TreeNode buildTreeByPreOrder(int[] preorder, int[] inorder){
    if (preorder == null){
        return null;
    }
    //由於咱們要在中序遍歷中尋找某個元素的位置,而後劃分左右子樹
    //用一個map來存儲元素在中序遍歷中的位置,
    Map<Integer,Integer> map = new HashMap<>();
    for(int i = 0; i < inorder.length; i++){
        map.put(inorder[i], i);
    }
    return buildTreeByPreOrder(preorder, inorder, 0, preorder.length-1, 0, inorder.length-1,map);
}
//傳入前序和中序,傳入前序的左右邊界,中序的左右邊界;
private static TreeNode buildTreeByPreOrder(int[] preorder, int[] inorder, int preleft, int preright, int inleft, int inright, Map<Integer,Integer> map){
    if (preleft > preright) return null;
    //得到整顆樹的根節點:先序中的第一個元素;
    TreeNode root = new TreeNode(preorder[preleft]);
    //獲得此元素在中序中的位置,以此進行劃分出左右子樹;
    int rootIndex = map.get(root);
    //獲得左子樹的大小;(注意此時不能直接是rootIndex,inleft不老是從0開始的,想一下創建右子樹的左子樹。
    int leftTreeSize = rootIndex - inleft;
    //左子樹的中序:inleft不變,inright爲rootIndex-1;
    //左子樹的前序:preleft爲根後一位,即preleft+1,preright爲根後leftTreeSize位,即preleft+leftTreeSize;
    root.left = buildTreeByPreOrder(preorder,inorder,preleft+1, preleft+leftTreeSize, inleft, rootIndex-1, map);
    //右子樹的中序:inleft爲rootIndex+1,inright不變;
    //右子樹的前序:preleft爲左子樹的右邊界+1,即preleft+leftTreeSize+1,preright不變;
    root.right = buildTreeByPreOrder(preorder,inorder,preleft+leftTreeSize+1, preright, rootIndex+1, inright,map);
    return root;
}

6.2 後序+中序

後序的遍歷順序是左右根,中序的遍歷順序是左根右,

遞歸實現
1.後序的第一個節點是root節點,對應可以找到在中序中的位置。
2.根據中序遍歷的特色,在找到的根前邊序列是左子樹的中序遍歷,後邊序列是右子樹的中序遍歷。
3.求出左邊序列的個數,好比設爲leftSize,那在後序序列中的leftSize個元素是左子樹的後序序列,後邊的爲右子樹的後序序列。
4.這樣就又得到了兩個子樹的後序遍歷和中序遍歷,開始遞歸。

/**
 * 根據後序遍歷和中序遍歷構造二叉樹
 */
public static TreeNode buildTreeByPostOrder(int[] postorder, int[] inorder){
    if (postorder == null){
        return null;
    }
    //由於咱們要在中序遍歷中尋找某個元素的位置,而後劃分左右子樹
    //用一個map來存儲元素在中序遍歷中的位置,
    Map<Integer,Integer> map = new HashMap<>();
    for(int i = 0; i < inorder.length; i++){
        map.put(inorder[i], i);
    }
    return buildTreeByPostOrder(inorder, postorder, 0, inorder.length-1, 0, postorder.length-1,map);
}
//傳入後序和中序,傳入後序的左右邊界,中序的左右邊界;
private static TreeNode buildTreeByPostOrder(int[] inorder, int[] postorder, int inleft, int inright, int postleft, int postright, Map<Integer,Integer> map){
    if (postleft > postright) return null;
    //得到整顆樹的根節點:後序中的最後個元素;
    TreeNode root = new TreeNode(postorder[postleft]);
    //獲得此元素在中序中的位置,以此進行劃分出左右子樹;
    int rootIndex = map.get(root.value);
    //獲得左子樹的大小;(注意此時不能直接是rootIndex,inleft不老是從0開始的,想一下創建右子樹的左子樹。
    int leftTreeSize = rootIndex - inleft;
    root.left = buildTreeByPostOrder(inorder, postorder, inleft, rootIndex-1,postleft,postleft+leftTreeSize-1,map);
    root.right = buildTreeByPostOrder(inorder,postorder,rootIndex+1, inright, postleft+leftTreeSize,postright-1,map);
    return root;
}

附錄(全程序)

package xin.utils;

import jdk.internal.dynalink.beans.StaticClass;
import sun.reflect.generics.tree.VoidDescriptor;

import javax.sound.midi.Soundbank;
import java.awt.font.TransformAttribute;
import java.util.*;

public class Tree {

}

/**
 * 定義一個二叉樹節點;
 */
class TreeNode<T>{
    public T value;  //數據;
    public TreeNode<T> left; //左子樹;
    public TreeNode<T> right;//右子樹;

    public TreeNode(){} //空參的;
    public TreeNode(T value){ //有參的;
        this.value = value;
    }
    public TreeNode(T value, TreeNode left, TreeNode right){
        this.value = value;
        this.left = left;
        this.right = right;
    }
}

class BinaryTreeTraverse<T> {

    /**
     * 前序遍歷(遞歸實現);
     */
    public static void preOrderByRecursion(TreeNode node) {
        if (node == null) return; //遞歸終止條件;
        System.out.println(node.value); //獲取根節點的值;
        preOrderByRecursion(node.left); //左子樹的根節點;
        preOrderByRecursion(node.right);//右子樹的根節點;
    }

    /**
     * 前序遍歷(非遞歸實現);
     * 本質上就是維持遞歸實現的棧;
     * 1.先入棧根節點,輸出根節點的值,再入棧其右節點,左節點;(爲了出棧的時候先出左節點,再出右節點);
     * 2.出棧左節點,輸出值,再入棧左節點的右節點、左節點;直到遍歷完左子樹;
     * 3.出棧右節點,輸出值,再入棧右節點的右節點、左節點;直到遍歷完右子樹;
     * 每次都是出棧一個根節點,若是有孩子,就依次入棧其右節點和左節點。
     * 規則:
     * 壓入根節點;
     * 1.彈出就打印;
     * 2.若有右孩子,壓入右;
     * 3.若有左孩子,壓入左;重複;
     * (至關於兩個孩子替換掉了棧裏的根節點,這就很符號:根先走了,左子樹幹到了最頂部,要是我左子樹還有孩子,ok,我也走,兩個孩子來替我,
     * 要是左子樹沒孩子了,我本身出去,我這裏就完事了;再去看右子樹就能夠了)
     */
    public static void preOrder(TreeNode node) {
        Stack<TreeNode> stack = new Stack<>();
        if (node != null) {
            stack.push(node); //根節點入棧;
        }
        while (!stack.isEmpty()) {
            TreeNode top = stack.pop(); //彈出就打印;
            System.out.println(top.value);
            if (top.right != null) stack.push(top.right); //依次入棧右節點和左節點;
            if (top.left != null) stack.push(top.left);
        }
    }

    /**
     * 中序遍歷(遞歸實現)
     */
    public static void inOrderByRecursion(TreeNode node) {
        if (node == null) return; //遞歸終止;
        inOrderByRecursion(node.left); //先左子樹;
        System.out.println(node.value); //再根節點;
        inOrderByRecursion(node.right); //再右節點;
    }

    /**
     * 中序遍歷(非遞歸實現)
     * 規則:
     * 1.整條左邊界依次壓棧;
     * 2.條件1執行不了,彈出就打印;
     * 3.來到彈出節點右樹上,繼續執行條件1;(右樹爲空,執行2.彈出打印;右樹不爲空,執行1;壓棧)
     * 說明:將整個樹全用左邊界去看,都是先處理了左邊界,將左邊界分解成了左頭,頭先入,左再入,而後弄不動了,彈出,而後看其右節點,再把右節點裏的左依次進去;就這樣往返;
     */
    public static void inOrder(TreeNode node) {
        Stack<TreeNode> stack = new Stack<>();
        while (!stack.isEmpty() || node != null) {
            if (node != null) { //條件1;能往左走就往左走;
                stack.push(node);
                node = node.left;
            } else {
                TreeNode top = stack.pop(); //條件2;彈出打印;
                System.out.println(top.value);
                node = top.right; //條件3;來彈出節點右樹上,繼續1;
            }
        }
    }

    /**
     * 後序遍歷(遞歸實現)
     */
    public static void postOrderByRecursion(TreeNode node) {
        if (node == null) return;
        postOrderByRecursion(node.left);
        postOrderByRecursion(node.right);
        System.out.println(node.value);
    }

    /**
     * 後序遍歷(非遞歸實現)
     * 想一下前序遍歷:根左右;過程是先壓右孩子,再壓左孩子;
     * 若是咱們想實現根右左:那就把前序裏的換成先壓左,再壓右;就處理成了 根右左;
     * 而後再從後往前看,就變成了右左根;因此能夠再準備一個棧,用來把第一個棧彈出的壓到第二個,那第二個彈出的時候就倒過來了;
     * 要記住:棧有實現倒序的功能;
     */
    public static void postOrder(TreeNode node) {
        Stack<TreeNode> stackA = new Stack<>();
        Stack<TreeNode> stackB = new Stack<>();
        if (node != null) {
            stackA.push(node);
        }
        while (!stackA.isEmpty()) {
            TreeNode top = stackA.pop();
            stackB.push(top);  //棧A彈出的進入棧B;先實現根右左,B倒序實現左右根;
            if (top.left != null) stackA.push(top.left);
            if (top.right != null) stackA.push(top.right);
        }
        while (!stackB.isEmpty()) {
            System.out.println(stackB.pop().value);
        }
    }

    /**
     * Morris遍歷;
     * 二叉樹的結構中只有父節點指向孩子節點,孩子節點不能向上指,因此須要棧。
     * 而morris遍歷的實質就是讓下層節點可以指向上層。怎麼辦呢,一個節點有兩個指針,左和右,若是這兩個指針上都有指向具體的節點確定就不行了。
     * 可是二叉樹上有不少空閒指針,好比全部的葉子節點,它們的指針就指向null,因此能夠利用其right指針指向上層的節點。
     * 這樣鏈接後,cur這個指針就能夠完整的順着一個鏈條遍歷完整個樹。
     * 核心:以某個根節點開始,找到它左子樹的最右側節點(必然是個葉子節點),而後利用其right指向根節點(完成向上層的返回)。
     * 到達兩次的是有左子樹的節點;
     * 到達一次的是沒有左子樹的節點;
     */
    public static void morris(TreeNode node) {
        if (node == null) {
            return;
        }
        TreeNode cur = node;
        TreeNode mostRight = null; //cur左子樹的最右節點;
        while (cur != null) {
            mostRight = cur.left;
            if (mostRight != null) { //cur有左子樹;
                //找到左子樹的最右節點;
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right; //1.右邊爲空了證實到頭了(找到了);2.右邊指向上層了證實以前就處理過了,結束;
                }
                //走到這裏證實跳出上面循環,無非兩個緣由:
                //1.右邊沒了;2.右邊指向上層了(以前就處理過了);
                if (mostRight.right == null) { //右邊走到頭了;
                    mostRight.right = cur; //左子樹的最右節點指向上層(cur);
                    cur = cur.left;        //cur左移,處理下一個節點;
                    continue; //這次循環結束,開始下一個cur;
                } else {  //證實這個mostRight的right指針已經處理過了,即mostRight已經指向了cur;
                    // 能到這裏說明咱們已經回到了根節點,而且重複了以前的操做;也說明咱們已經徹底處理完了此根節點左邊的的樹了,把路斷開;
                    mostRight.right = null;
                }
            }
            //cur右移的狀況:
            //1.cur沒有左子樹了(天然要開始處理右子樹)
            //2.cur有左子樹,可是cur左子樹最右節點已經指向cur了(執行完上面else斷開後,cur左邊已經徹底處理好了,開始右移。)
            cur = cur.right;
        }
    }

    /**
     * 前序遍歷(Morris實現);
     * 1.對於cur只到達一次的節點(沒有左子樹),cur到達就打印;
     * 2.對於cur到達兩次的節點,到達第一次時打印;
     */
    public static void preOrderMorris(TreeNode node) {
        if (node == null) {
            return;
        }
        TreeNode cur = node;
        TreeNode mostRightNode = null;
        while (cur != null) {
            mostRightNode = cur.left;
            if (mostRightNode != null) { //到達兩次的節點;
                //找到cur左子樹的最右節點;
                while (mostRightNode.right != null && mostRightNode.right != cur) {
                    mostRightNode = mostRightNode.right;
                }
                if (mostRightNode.right == null) {
                    mostRightNode.right = cur;  //指向上層cur;
                    System.out.println(cur.value); //第一次到的時候打印;
                    cur = cur.left;
                    continue;
                } else {
                    mostRightNode.right = null; //第二次到時不打印;
                }
            } else {
                System.out.println(cur.value); //只到達一次的節點;
            }
            cur = cur.right;
        }
    }

    /**
     * 中序遍歷(Morris實現);
     * 1.對於cur只到達一次的節點(沒有左子樹),cur到達就打印;
     * 2.對於cur到達兩次的節點,到達第二次時打印;
     */
    public static void inOrderMorris(TreeNode node) {
        if (node == null) {
            return;
        }
        TreeNode cur = node;
        TreeNode mostRightNode = null;
        while (cur != null) {
            mostRightNode = cur.left;
            if (mostRightNode != null) { //有左子樹,到達兩次的節點;
                while (mostRightNode.right != null && mostRightNode.right != cur) {
                    mostRightNode = mostRightNode.right;
                }
                if (mostRightNode == null) {
                    mostRightNode.right = cur; //第一次不打印;
                    cur = cur.left;
                    continue;
                } else {
                    System.out.println(cur.value); //第二次遇到時打印;
                    mostRightNode.right = null;
                }
            } else {
                System.out.println(cur.value); //只到達一次的節點,遇到就打印;
            }
            cur = cur.right;
        }
    }

    /**
     * 後序遍歷(morris實現)
     * 後序遍歷比前面兩個要複雜一點;
     * 將一個節點的連續右節點當作是一個單鏈表來看待,
     * 當返回上層後,也就是將創建的連線斷開後,打印下層的單鏈表;
     * 單鏈表逆序打印,就和咱們作的把單鏈表逆序同樣。
     */
    public static void postOrderMorris(TreeNode node) {
        if (node == null) {
            return;
        }
        TreeNode cur = node;
        TreeNode mostRightNode = null;
        while (node != null) {
            mostRightNode = cur.left;
            if (mostRightNode != null) {
                while (mostRightNode.right != null && mostRightNode.right != cur) {
                    mostRightNode = mostRightNode.right;
                }
                if (mostRightNode.right == null) {
                    mostRightNode.right = cur;
                    cur = cur.left;
                    continue;
                } else { //能到這裏的都是達到過兩次的,也就是是有左孩子的。
                    mostRightNode.right = null; //這時候是已經返回上層以後,斷開了鏈接,因此打印下層的單鏈表;
                    postMorrisPrint(cur.left);
                }
            }
            cur = cur.right;
        }
        postMorrisPrint(node); //最後把頭節點那一串右打印一遍;
    }

    public static void postMorrisPrint(TreeNode node) {
        TreeNode reverseList = postMorrisReverseList(node); //反轉鏈表;
        TreeNode cur = reverseList;
        while (cur != null) {
            System.out.println(cur.value);
            cur = cur.right;
        }
        postMorrisReverseList(reverseList); //最後再還原;
    }

    public static TreeNode postMorrisReverseList(TreeNode node) {
        TreeNode cur = node;
        TreeNode pre = null;
        while (cur != null) {
            TreeNode next = cur.right;
            cur.right = pre;
            pre = cur;
            cur = next;
        }
        return pre;
    }

    /**
     * 層次遍歷
     * 藉助隊列的結構,每一層依次入隊,再依次出隊;
     * 對該層節點進行出隊操做時,須要將該節點的左孩子和右孩子入隊;
     */
    public static void layerOrder(TreeNode node) {
        if (node == null) return;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(node);
        while (!queue.isEmpty()) {
            int size = queue.size(); //當前層的節點數量;
            for (int i = 0; i < size; i++) {
                TreeNode front = queue.poll();
                System.out.println(front.value);
                if (front.left != null) queue.add(front.left);
                if (front.right != null) queue.add(front.right);
            }
        }
    }
}
class Depth{

    /**
     * 求最大深度(遞歸)
     * 最大深度是左子樹和右子樹的最大深度的大的那個+1;
     */
    public static int maxDepthByRecursion(TreeNode node){
        if(node == null) return 0;
        int leftDepth = maxDepthByRecursion(node.left);
        int rightDepth = maxDepthByRecursion(node.right);
        return Math.max(leftDepth,rightDepth)+1;
    }

    /**
     * 求最大深度(非遞歸)
     * 層次遍歷(BFS)
     * 每遍歷一層,則計數器加+1;直到遍歷完成,獲得樹的深度。
     */
    public static int maxDepth(TreeNode node){
        if (node == null) return 0;
        Queue<TreeNode> queue = new LinkedList<>();
        int level = 0; //層數;
        queue.add(node);
        while (!queue.isEmpty()){
            level++;
            int levelNum = queue.size(); //每層的節點數;
            for (int i = 0; i < levelNum; i++){
                TreeNode front = queue.poll(); //當前層出隊,下一層入隊;
                if (front.left != null) queue.add(front.left);
                if (front.right != null) queue.add(front.right);
            }
        }
        return level;
    }

    /**
     * 求最小深度(遞歸)
     * 注意和求最大深度的區別;
     */
    public static int minDepthByRecursion(TreeNode node){
        if (node == null) return 0;
        if (node.right == null && node.left == null) return 1;
        if (node.left == null && node.right != null) return minDepthByRecursion(node.right) + 1;
        if (node.right == null && node.left != null) return minDepthByRecursion(node.left) + 1;
        return Math.min(minDepthByRecursion(node.left), minDepthByRecursion(node.right))+1;
    }

    /**
     * 求最小深度(非遞歸)
     */
    public static int minDepth(TreeNode node){
        if (node == null) return 0;
        Queue<TreeNode> queue = new LinkedList<>();
        int level = 0;
        queue.add(node);
        while (!queue.isEmpty()){
            level++;
            int levelnum = queue.size();
            for(int i = 0; i < levelnum; i++){
                TreeNode front = queue.poll();
                if (front.left == null && front.right == null){
                    return level; //遇到第一個無葉子節點的時候,該節點的深度爲最小深度;
                }
                if (front.right != null){
                    queue.add(front.right);
                }
                if (front.left != null){
                    queue.add(front.left);
                }
            }
        }
        return level;
    }
}

class BuildBinaryTree{

    /**
     * 根據前序遍歷和中序遍歷構造二叉樹;
     */
    public static TreeNode buildTreeByPreOrder(int[] preorder, int[] inorder){
        if (preorder == null){
            return null;
        }
        //由於咱們要在中序遍歷中尋找某個元素的位置,而後劃分左右子樹
        //用一個map來存儲元素在中序遍歷中的位置,
        Map<Integer,Integer> map = new HashMap<>();
        for(int i = 0; i < inorder.length; i++){
            map.put(inorder[i], i);
        }
        return buildTreeByPreOrder(preorder, inorder, 0, preorder.length-1, 0, inorder.length-1,map);
    }
    //傳入前序和中序,傳入前序的左右邊界,中序的左右邊界;
    private static TreeNode buildTreeByPreOrder(int[] preorder, int[] inorder, int preleft, int preright, int inleft, int inright, Map<Integer,Integer> map){
        if (preleft > preright) return null;
        //得到整顆樹的根節點:先序中的第一個元素;
        TreeNode root = new TreeNode(preorder[preleft]);
        //獲得此元素在中序中的位置,以此進行劃分出左右子樹;
        int rootIndex = map.get(root.value);
        //獲得左子樹的大小;(注意此時不能直接是rootIndex,inleft不老是從0開始的,想一下創建右子樹的左子樹。
        int leftTreeSize = rootIndex - inleft;
        //左子樹的中序:inleft不變,inright爲rootIndex-1;
        //左子樹的前序:preleft爲根後一位,即preleft+1,preright爲根後leftTreeSize位,即preleft+leftTreeSize;
        root.left = buildTreeByPreOrder(preorder,inorder,preleft+1, preleft+leftTreeSize, inleft, rootIndex-1, map);
        //右子樹的中序:inleft爲rootIndex+1,inright不變;
        //右子樹的前序:preleft爲左子樹的右邊界+1,即preleft+leftTreeSize+1,preright不變;
        root.right = buildTreeByPreOrder(preorder,inorder,preleft+leftTreeSize+1, preright, rootIndex+1, inright,map);
        return root;
    }

    /**
     * 根據後序遍歷和中序遍歷構造二叉樹
     */
    public static TreeNode buildTreeByPostOrder(int[] postorder, int[] inorder){
        if (postorder == null){
            return null;
        }
        //由於咱們要在中序遍歷中尋找某個元素的位置,而後劃分左右子樹
        //用一個map來存儲元素在中序遍歷中的位置,
        Map<Integer,Integer> map = new HashMap<>();
        for(int i = 0; i < inorder.length; i++){
            map.put(inorder[i], i);
        }
        return buildTreeByPostOrder(inorder, postorder, 0, inorder.length-1, 0, postorder.length-1,map);
    }
    //傳入後序和中序,傳入後序的左右邊界,中序的左右邊界;
    private static TreeNode buildTreeByPostOrder(int[] inorder, int[] postorder, int inleft, int inright, int postleft, int postright, Map<Integer,Integer> map){
        if (postleft > postright) return null;
        //得到整顆樹的根節點:後序中的最後個元素;
        TreeNode root = new TreeNode(postorder[postleft]);
        //獲得此元素在中序中的位置,以此進行劃分出左右子樹;
        int rootIndex = map.get(root.value);
        //獲得左子樹的大小;(注意此時不能直接是rootIndex,inleft不老是從0開始的,想一下創建右子樹的左子樹。
        int leftTreeSize = rootIndex - inleft;
        root.left = buildTreeByPostOrder(inorder, postorder, inleft, rootIndex-1,postleft,postleft+leftTreeSize-1,map);
        root.right = buildTreeByPostOrder(inorder,postorder,rootIndex+1, inright, postleft+leftTreeSize,postright-1,map);

        return root;
    }
}

參考連接

史上最全遍歷二叉樹詳解
二叉樹總結
算法基地-二叉樹

相關文章
相關標籤/搜索