結點的度(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
如圖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); } }
由於要在遍歷完節點的左子樹後接着遍歷節點的右子樹,爲了能找到該節點,須要使用棧來進行暫存。中序和後序也都涉及到回溯,因此都須要用到棧。
遍歷過程參考註釋
// 非遞歸先序遍歷 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所示,從節點1開始考查直到節點4的左子樹爲空。
注:此時的遊標節點node = 4.left == null。
此時須要從棧中查看 Peek()棧頂元素。
發現節點4的右子樹非空,須要接着考查右子樹,4不能輸出,node = node.right。
如圖4所示,考查到節點7(7.left == null,7是從棧中彈出),其左右子樹都爲空,能夠直接輸出7。
此時須要把lastVisit設置成節點7,並把遊標節點node設置成null,下一輪循環的時候會考查棧中的節點6。
如圖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
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) }