玩透二叉樹(Binary-Tree)及前序(先序)、中序、后序【遞歸和非遞歸】遍歷


 

 


基礎預熱:

 

 

結點的度(Degree):結點的子樹個數;
樹的度:樹的所有結點中最大的度數;
葉結點(Leaf):度為0的結點;
父結點(Parent):有子樹的結點是其子樹的根節點的父結點;
子結點/孩子結點(Child):若A結點是B結點的父結點,則稱B結點是A結點的子結點;
兄弟結點(Sibling):具有同一個父結點的各結點彼此是兄弟結點;
路徑和路徑長度:從結點n1到nk的路徑為一個結點序列n1,n2,…,nk。ni是ni+1的父結點。路徑所包含邊的個數為路徑的長度;
祖先結點(Ancestor):沿樹根到某一結點路徑上的所有結點都是這個結點的祖先結點;
子孫結點(Descendant):某一結點的子樹中的所有結點是這個結點的子孫;
結點的層次(Level):規定根結點在1層,其他任一結點的層數是其父結點的層數加1;
樹的深度(Depth):樹中所有結點中的最大層次是這棵樹的深度;

 

 

 

 

滿二叉樹

除最后一層無任何子節點外,每一層上的所有結點都有兩個子結點二叉樹。

完全二叉樹

一棵二叉樹至多只有最下面的一層上的結點的度數可以小於2,並且最下層上的結點都集中在該層最左邊的若干位置上,則此二叉樹成為完全二叉樹。

平衡二叉樹

它是一 棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹

 

前序、中序、后序

首先給出二叉樹節點類:

樹節點:

class TreeNode { int val; //左子樹 TreeNode left; //右子樹 TreeNode right; //構造方法 TreeNode(int x) { val = x; } } 

無論是哪種遍歷方法,考查節點的順序都是一樣的(思考做試卷的時候,人工遍歷考查順序)。只不過有時候考查了節點,將其暫存,需要之后的過程中輸出。

 
圖2:先序、中序、后序遍歷節點考查順序

如圖1所示,三種遍歷方法(人工)得到的結果分別是:

先序:1 2 4 6 7 8 3 5
中序:4 7 6 8 2 1 3 5
后序:7 8 6 4 2 5 3 1

三種遍歷方法的考查順序一致,得到的結果卻不一樣,原因在於:

先序:考察到一個節點后,即刻輸出該節點的值,並繼續遍歷其左右子樹。(根左右)

中序:考察到一個節點后,將其暫存,遍歷完左子樹后,再輸出該節點的值,然后遍歷右子樹。(左根右)

后序:考察到一個節點后,將其暫存,遍歷完左右子樹后,再輸出該節點的值。(左右根)


先序遍歷

遞歸先序遍歷

遞歸先序遍歷很容易理解,先輸出節點的值,再遞歸遍歷左右子樹。中序和后序的遞歸類似,改變根節點輸出位置即可。

// 遞歸先序遍歷 public static void recursionPreorderTraversal(TreeNode root) { if (root != null) { System.out.print(root.val + " "); recursionPreorderTraversal(root.left); recursionPreorderTraversal(root.right); } } 

非遞歸先序遍歷

因為要在遍歷完節點的左子樹后接着遍歷節點的右子樹,為了能找到該節點,需要使用棧來進行暫存。中序和后序也都涉及到回溯,所以都需要用到棧。

 
圖2:非遞歸先序遍歷

遍歷過程參考注釋

// 非遞歸先序遍歷 public static void preorderTraversal(TreeNode root) { // 用來暫存節點的棧 Stack<TreeNode> treeNodeStack = new Stack<TreeNode>(); // 新建一個游標節點為根節點 TreeNode node = root; // 當遍歷到最后一個節點的時候,無論它的左右子樹都為空,並且棧也為空 // 所以,只要不同時滿足這兩點,都需要進入循環 while (node != null || !treeNodeStack.isEmpty()) { // 若當前考查節點非空,則輸出該節點的值 // 由考查順序得知,需要一直往左走 while (node != null) { System.out.print(node.val + " "); // 為了之后能找到該節點的右子樹,暫存該節點 treeNodeStack.push(node); node = node.left; } // 一直到左子樹為空,則開始考慮右子樹 // 如果棧已空,就不需要再考慮 // 彈出棧頂元素,將游標等於該節點的右子樹 if (!treeNodeStack.isEmpty()) { node = treeNodeStack.pop(); node = node.right; } } } 

先序遍歷結果:

遞歸先序遍歷: 1 2 4 6 7 8 3 5
非遞歸先序遍歷:1 2 4 6 7 8 3 5


中序遍歷

遞歸中序遍歷

過程和遞歸先序遍歷類似

// 遞歸中序遍歷 public static void recursionMiddleorderTraversal(TreeNode root) { if (root != null) { recursionMiddleorderTraversal(root.left); System.out.print(root.val + " "); recursionMiddleorderTraversal(root.right); } } 

非遞歸中序遍歷

和非遞歸先序遍歷類似,唯一區別是考查到當前節點時,並不直接輸出該節點。

而是當考查節點為空時,從棧中彈出的時候再進行輸出(永遠先考慮左子樹,直到左子樹為空才訪問根節點)。

// 非遞歸中序遍歷 public static void middleorderTraversal(TreeNode root) { Stack<TreeNode> treeNodeStack = new Stack<TreeNode>(); TreeNode node = root; while (node != null || !treeNodeStack.isEmpty()) { while (node != null) { treeNodeStack.push(node); node = node.left; } if (!treeNodeStack.isEmpty()) { node = treeNodeStack.pop(); System.out.print(node.val + " "); node = node.right; } } } 

中序遍歷結果

遞歸中序遍歷: 4 7 6 8 2 1 3 5
非遞歸中序遍歷:4 7 6 8 2 1 3 5


后序遍歷

遞歸后序遍歷

過程和遞歸先序遍歷類似

// 遞歸后序遍歷 public static void recursionPostorderTraversal(TreeNode root) { if (root != null) { recursionPostorderTraversal(root.left); recursionPostorderTraversal(root.right); System.out.print(root.val + " "); } } 

非遞歸后序遍歷

后續遍歷和先序、中序遍歷不太一樣。

后序遍歷在決定是否可以輸出當前節點的值的時候,需要考慮其左右子樹是否都已經遍歷完成。

所以需要設置一個lastVisit游標。

若lastVisit等於當前考查節點的右子樹,表示該節點的左右子樹都已經遍歷完成,則可以輸出當前節點。

並把lastVisit節點設置成當前節點,將當前游標節點node設置為空,下一輪就可以訪問棧頂元素。

否者,需要接着考慮右子樹,node = node.right。

以下考慮后序遍歷中的三種情況:

 
圖3:后序,右子樹不為空,node = node.right

如圖3所示,從節點1開始考查直到節點4的左子樹為空。

注:此時的游標節點node = 4.left == null。

此時需要從棧中查看 Peek()棧頂元素。

發現節點4的右子樹非空,需要接着考查右子樹,4不能輸出,node = node.right。

 
圖4:后序,左右子樹都為空,直接輸出

如圖4所示,考查到節點7(7.left == null,7是從棧中彈出),其左右子樹都為空,可以直接輸出7。

此時需要把lastVisit設置成節點7,並把游標節點node設置成null,下一輪循環的時候會考查棧中的節點6。

 
圖5:后序,右子樹 = lastVisit,直接輸出

如圖5所示,考查完節點8之后(lastVisit == 節點8),將游標節點node賦值為棧頂元素6,節點6的右子樹正好等於節點8。表示節點6的左右子樹都已經遍歷完成,直接輸出6。

此時,可以將節點直接從棧中彈出Pop(),之前用的只是Peek()。

將游標節點node設置成null。

// 非遞歸后序遍歷 public static void postorderTraversal(TreeNode root) { Stack<TreeNode> treeNodeStack = new Stack<TreeNode>(); TreeNode node = root; TreeNode lastVisit = root; while (node != null || !treeNodeStack.isEmpty()) { while (node != null) { treeNodeStack.push(node); node = node.left; } //查看當前棧頂元素 node = treeNodeStack.peek(); //如果其右子樹也為空,或者右子樹已經訪問 //則可以直接輸出當前節點的值 if (node.right == null || node.right == lastVisit) { System.out.print(node.val + " "); treeNodeStack.pop(); lastVisit = node; node = null; } else { //否則,繼續遍歷右子樹 node = node.right; } } } 

后序遍歷結果

遞歸后序遍歷: 7 8 6 4 2 5 3 1
非遞歸后序遍歷:7 8 6 4 2 5 3 1

完整算法、用例 by Golang

package main

import "fmt"

type Node struct {
    V int
    L *Node
    R *Node
}
//前序
func forwardLook(root *Node)  {
    if root == nil {
        return
    }
    //輸出行的位置在最前面
    fmt.Printf("node %v ", root.V)
    forwardLook(root.L)
    forwardLook(root.R)
}
//var i int
func forwardLoop(root *Node) {
    //需要一個堆保存走過的路徑
    nodes:=[]*Node{}
    for len(nodes) != 0 || root != nil {
        //一直往左走
        for root != nil{
            nodes=append(nodes, root)
            fmt.Printf("node %v ",root.V)
            root = root.L
        }
        //說明左子結點為空,那么就看右結點
        if len(nodes) >0 {
            root=nodes[len(nodes)-1]
            //用完最近一個結點后,刪除它,刪除后最后的結點一定是父結點
            nodes=nodes[:len(nodes)-1]
            //左子結點遍歷完了,所以這里只看當看結點的右子結點
            root=root.R
        }else{
            root = nil
        }
    }
}
//中序
func middleLook(root *Node)  {
    if root == nil {
        return
    }
    middleLook(root.L)
    //輸出行的位置在中間
    fmt.Printf("node %v ", root.V)
    middleLook(root.R)
}
//后序
func backwardLook(root *Node)  {
    if root == nil {
        return
    }
    //輸出行的位置在后面
    backwardLook(root.L)
    backwardLook(root.R)
    fmt.Printf("node %v ", root.V)
}

func main(){
    tree:=&Node{1,
        &Node{2,
            &Node{4, nil, nil}, &Node{5, nil, nil},
            },
        &Node{3,
            &Node{6, nil, nil}, &Node{7, nil, nil},
            },
    }
    fmt.Println("\nforwardLook ")
    forwardLook(tree)
    fmt.Println("\nforwardLoop ")
    forwardLoop(tree)
    fmt.Println("\nmiddleLook ")
    middleLook(tree)
    fmt.Println("\nbackwardLook ")
    backwardLook(tree)


    tree=&Node{1,
        &Node{2,
            nil,
            &Node{4,
                nil,
                &Node{6,
                    &Node{7, nil, nil},
                    &Node{8, nil, nil},
                    },
                },
            },
        &Node{3,
            nil, &Node{5, nil, nil},
            },
    }
    fmt.Println("\nforwardLook ")
    forwardLook(tree)
    fmt.Println("\nforwardLoop ")
    forwardLoop(tree)
    fmt.Println("\nmiddleLook ")
    middleLook(tree)
    fmt.Println("\nbackwardLook ")
    backwardLook(tree)
}

 

總結

 


免責聲明!

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



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