寫在前頭的總結
下面的先序中序後序遍歷以及二叉樹的帶權路徑的解法基本可以囊括絕大多數與二叉樹相關的問題,爲了方便記憶,在寫的時候都用了兩個嵌套的while循環,省得到時候再跟while裏套if else的記混~
首先二叉樹的先序、中序、後序遍歷在 這篇博客 裏寫的很清楚,尤其是後續遍歷,思路很巧妙:後序遍歷的非遞歸版本是最有技巧性的,難度相對高一點,但其實是可以轉換成前序遍歷的。後序遍歷的順序是左右中,因此只需要按中右左遍歷,再reverse一下即可。 當然大部分玩家實現的方式都是兩個棧如下:
import java.util.Stack;
public class BinaryTree1 {
public Node init() {//注意必須逆序建立,先建立子節點,再逆序往上建立,因爲非葉子結點會使用到下面的節點,而初始化是按順序初始化的,不逆序建立會報錯
Node J = new Node(8, null, null);
Node H = new Node(4, null, null);
Node G = new Node(2, null, null);
Node F = new Node(7, null, J);
Node E = new Node(5, H, null);
Node D = new Node(1, null, G);
Node C = new Node(9, F, null);
Node B = new Node(3, D, E);
Node A = new Node(6, B, C);
return A; //返回根節點
}
public void printNode(Node node){
System.out.print(node.getData());
}
public void theFirstTraversal_Stack(Node root) { //先序遍歷
Stack<Node> stack = new Stack<>();
Node node = root;
while (node != null || stack.size() > 0) { //將所有左孩子壓棧
while(node != null) { //壓棧之前先訪問
printNode(node);
stack.push(node);
node = node.getLeftNode();
}
/*
重要的兩行:pop()後,最近的一個被訪問了左子樹的節點被彈出棧,
然後將當前操作的節點設置爲剛剛被彈出棧的節點的右子節點。
*/
node = stack.pop();
node = node.getRightNode();
}
}
public void theInOrderTraversal_Stack(Node root) { //中序遍歷
Stack<Node> stack = new Stack<>();
Node node = root;
while (node != null || stack.size() > 0) {
while (node != null) {
stack.push(node);
node = node.getLeftNode();
}
node = stack.pop();
printNode(node);
node = node.getRightNode();
}
}
public void thePostOrderTraversal_Stack(Node root) { //後序遍歷
Stack<Node> stack = new Stack<>();
Stack<Node> output = new Stack<>();//構造一箇中間棧來存儲逆後序遍歷的結果
Node node = root;
while (node != null || stack.size() > 0) {
if (node != null) {
output.push(node);
stack.push(node);
node = node.getRightNode();
} else {
node = stack.pop();
node = node.getLeftNode();
}
}
System.out.println(output.size());
while (output.size() > 0) {
printNode(output.pop());
}
}
public static void main(String[] args) {
BinaryTree1 tree = new BinaryTree1();
Node root = tree.init();
System.out.println("先序遍歷");
tree.theFirstTraversal_Stack(root);
System.out.println("");
System.out.println("中序遍歷");
tree.theInOrderTraversal_Stack(root);
System.out.println("");
System.out.println("後序遍歷");
tree.thePostOrderTraversal_Stack(root);
System.out.println("");
}
class Node{
private int data;
private Node leftNode;
private Node rightNode;
public Node(int data, Node leftNode, Node rightNode){
this.data = data;
this.leftNode = leftNode;
this.rightNode = rightNode;
}
public int getData() {
return data;
}
public Node getLeftNode() {
return leftNode;
}
public Node getRightNode() {
return rightNode;
}
}
}
二叉樹的帶權路徑(這個題目我幾個星期前被度小滿金融面試官問到了...)的計算也可以用非遞歸的方式來實現,但是有一點複雜、難想,找了很久纔在LeetCode上找了一個大佬的思路。首先該題目是,給定一個二叉樹和一個整數,求該二叉樹的跟到任一葉子節點的路徑和爲該整數的所有組合,比如:
看到這個題目第一反應必然是二叉樹的遍歷。這個題目用遞歸來做並不太難,非遞歸的話就有一點難想了,主要是因爲需要把之前遍歷過的節點保存下來,在由子節點回到父節點的時候要記得把子節點的路徑值減掉。如果使用上面鏈接裏的博客的做法的話,會丟失前面的訪問信息,導致根本不知道訪問到的是哪一層。
正確的解法同樣是像上面先序遍歷一樣使用兩個while循環,但是具體的部分會有差異,最精髓的地方在於:設置一個prev,在某節點遍歷完畢返回上層節點之前把pre設置爲自己,這樣上層節點就會先檢查自己的右節點是不是pre,以判斷本節點是不是也可以返回了;另外就是在循環的最後cur = null,跳過下一輪大循環開頭的小循環,避免重複訪問。具體請看下面代碼中的註釋:
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;
public class PathSumNoRecursive {
class TreeNode {
int val;
public TreeNode left;
public TreeNode right;
public TreeNode(int x) { val = x; }
}
public List<List<Integer>> pathSum(TreeNode root, int target) {
List<List<Integer>> res = new ArrayList<>();
if (root == null) return res;
LinkedList<Integer> list = new LinkedList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root, prev = null;
int sum = 0;
while (cur != null || !stack.isEmpty()) {
while (cur != null) {
// 左子節點入棧。同時把當前節點路徑值加進sum和list中
stack.push(cur);
sum += cur.val;
list.add(cur.val);
cur = cur.left;
}
cur = stack.peek(); // 重要,先peek再根據信息判斷是否還要繼續在棧裏,當不需要的時候再pop
if (cur.left == null && cur.right == null && sum == target){
//關鍵。add一個根據list而新生成的LinkedList,這樣還可以繼續用list
res.add(new LinkedList<>(list));
}
if (cur.right != null && cur.right != prev) {
// 右節點不爲空且沒有被訪問過
/*
這裏並不需要令pre = cur,因爲pre的作用是在
退回上一層節點時判斷當前節點的右子節點是否爲pre,即是否已訪問過。
*/
cur = cur.right;
}else {
// 左右都爲空 或者 雖然右節點不爲空但是已被訪問過了
prev = stack.pop();
sum -= prev.val; //返回時把自己的路徑值減掉
list.removeLast(); // 返回時把自己在鏈表中的位置刪除
/*
* 下一行很關鍵,在下一次的大循環中跳過小循環,是爲了
* 避免對已經訪問過左子節點的節點進行重新訪問。
*/
cur = null;
}
}
return res;
}
}
該方法同樣可以計算樹的深度(只要把所有節點的路徑值設爲1,就可以在遍歷結束後獲得最大深度了~)。查了百度的絕大多數的人都是用層次遍歷實現的使用廣度優先策略計算樹的深度,而本篇文章裏的方法是深度優先的~