玩透二叉樹(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):樹中全部結點中的最大層次是這棵樹的深度;node

 

 

 

 

滿二叉樹算法

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

徹底二叉樹oop

一棵二叉樹至多隻有最下面的一層上的結點的度數能夠小於2,而且最下層上的結點都集中在該層最左邊的若干位置上,則此二叉樹成爲徹底二叉樹。post

平衡二叉樹spa

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

 

前序、中序、後序

首先給出二叉樹節點類:

樹節點:code

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

不管是哪一種遍歷方法,考查節點的順序都是同樣的(思考作試卷的時候,人工遍歷考查順序)。只不過有時候考查了節點,將其暫存,須要以後的過程當中輸出。orm

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

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

先序: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)
}

 

總結

相關文章
相關標籤/搜索