【經典結構】二叉樹


二叉樹

1.基本概念

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

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

2.性質

1.完全二叉樹除了最后一層外,下一層節點個數是上一層兩倍,
如果一顆完全二叉樹的節點總數是n,那么葉子節點個數為n/2(n為偶數)或(n+1)/2(n為奇數);

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

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

寫樹相關的算法,簡單的就是說,去想象一個最小單元,搞清楚當前最小單元中t節點“該做什么,什么時候做”這兩點,然后根據函數定義遞歸調用子節點。

  • 該做什么:就是我們的最小單元中的root節點(可能不止一個)想要實現功能,需要得到什么信息,然后能做什么;
    • 做點什么能夠提供信息給下面的子樹利用(先序)
    • 能從下面的子樹上獲得什么信息然后利用(后序)
  • 什么時候做:剛才寫的代碼應該放在前序、中序還是后序的代碼位置上。

把題目的要求細化,搞清楚根節點應該做什么,然后剩下的事情拋給前/中/后序的遍歷框架就行了,難點在於如何通過題目的要求思考出每一個節點需要做什么。
寫完之后自己代入一個最基本的看能不能實現功能,會不會出不來,檢驗一下。

4.遍歷

4.1 概念

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

4.2 遞歸實現

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

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),遞歸過程中棧的開銷;

4.3 迭代實現

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

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.壓棧;
說明:將整個樹全用左邊界去看,都是先處理了左邊界,將左邊界分解成了左頭,頭先入,左再入,然后弄不動了,彈出,然后看其右節點,再把右節點里的左依次進去;就這樣往返;
如下如所示;

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)【圖中有4到2】
  • 2.如果cur有左孩子,找到cur左子樹上最右的節點,記為mostRightNode;
    • 1.如果mostRightNode的right指針指向空,讓其指向cur,cur向左移動(cur=cur.left)
    • 2.如果mostRightNode的right指針指向cur,讓其指向空,cur向右移動(cur=cur.right)【圖中由2到5】

(迭代法的中序遍歷我們可以將整個樹全部分成左邊界去看,如上面的圖,其實在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 (cur != 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 (cur == 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;
    }
}

參考鏈接

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM