基礎預熱:
結點的度(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; } }
無論是哪種遍歷方法,考查節點的順序都是一樣的(思考做試卷的時候,人工遍歷考查順序)。只不過有時候考查了節點,將其暫存,需要之后的過程中輸出。

如圖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); } }
非遞歸先序遍歷
因為要在遍歷完節點的左子樹后接着遍歷節點的右子樹,為了能找到該節點,需要使用棧來進行暫存。中序和后序也都涉及到回溯,所以都需要用到棧。

遍歷過程參考注釋
// 非遞歸先序遍歷 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
完整算法、用例 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) }
總結