算法練習(11)-二叉樹的各種遍歷


二叉樹的節點結構如下:

public class TreeNode {

    public TreeNode left;
    public TreeNode right;
    public int val;

    public TreeNode(int val) {
        this.val = val;
    }

    public TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }

    @Override
    public String toString() {
        return this.val + "";
    }
}

一、遞歸序

二叉樹的三種經典遍歷: 前序/中序/后序 可參考先前的文章:數據結構C#版筆記--樹與二叉樹,  不過今天換一種角度來理解"前序/中序/后序"(來自左程雲大佬的視頻分享), 假設有一個遞歸方法, 可以遍歷二叉樹:

public static void foo(TreeNode n1) {
    if (n1 == null) {
        return;
    }

    System.out.printf("(1):" + n1.val + "  ");

    foo(n1.left);
    System.out.printf("(2):" + n1.val + "  ");

    foo(n1.right);
    System.out.printf("(3):" + n1.val + "  ");

}

如上圖,可以看到,每個節點有3次被訪問到的時機,第1次是遞歸壓入堆棧,另外2次是左、右子節點處理完畢,函數返回。

如果在這3個時機,均打印節點的值,會發現:第1次打印的值(上圖底部的紅色輸出),就是前序遍歷(頭-左-右),第2次打印的值(上圖底部的藍色輸出),就是中間遍歷(左-頭-右),第3次打印的值(上圖底部的黑色輸出),就是后序遍歷(左-右-頭).這3次打印結果的全集, 也稱為"遞歸序".
 
 
二、前序/中序/ 后序遍歷的非遞歸實現
/**
 * 前序遍歷(非遞歸版): root-left-right
 *
 * @param root
 */
static void preOrderUnRecur(TreeNode root) {
    if (root == null) {
        return;
    }
    Stack<TreeNode> stack = new Stack<>();
    stack.add(root);
    while (!stack.isEmpty()) {
        TreeNode n = stack.pop();
        System.out.print(n.val + " ");
        if (n.right != null) {
            stack.add(n.right);
        }
        if (n.left != null) {
            stack.add(n.left);
        }
    }
}

/**
 * 中序遍歷(非遞歸版): left-root-right
 * 思路: 不停壓入左邊界(即:頭-左),直到null,
 *       然后彈出打印過程中,發現有右孩子,則壓棧
 *       然后再對右孩子,不停壓入左邊界
 * @param n
 */
static void inOrderUnRecur(TreeNode n) {
    Stack<TreeNode> stack = new Stack<>();
    while (n != null || !stack.isEmpty()) {
        if (n != null) {
            //左邊界進棧,直到最末端
            stack.push(n);
            n = n.left;
        } else {
            //跳到右邊,壓入右節點(壓完后,n不為空,會重新進入上面的左邊界處理)
            n = stack.pop();
            System.out.print(n.val + " ");
            n = n.right;
        }
    }
}

/**
 * 后序遍歷(非遞歸版): left-right-root
 *
 * @param root
 */
static void postOrderUnRecur(TreeNode root) {
    if (root == null) {
        return;
    }
    Stack<TreeNode> stack = new Stack<>();
    //用於收集最后所有"排好序"的節點
    Stack<TreeNode> result = new Stack<>();
    stack.add(root);
    while (!stack.isEmpty()) {
        TreeNode n = stack.pop();
        result.add(n);
        if (n.left != null) {
            stack.add(n.left);
        }
        if (n.right != null) {
            stack.add(n.right);
        }
    }
    while (!result.isEmpty()) {
        System.out.print(result.pop().val + " ");
    }
}

  

三、層序遍歷
即按一層層遍歷所有節點, 直接按頭-左-右, 放到隊列即可
public static void levelOrder(TreeNode n1) {
    if (n1 == null) {
        return;
    }
    Queue<TreeNode> queue = new LinkedList<>();
    queue.add(n1);
    while (!queue.isEmpty()) {
        TreeNode node = queue.poll();
        System.out.printf(node.val + " ");
        if (node.left != null) {
            queue.add(node.left);
        }
        if (node.right != null) {
            queue.add(node.right);
        }
    }
}

還是這顆樹,層序遍歷輸出結果為 1 2 3 4 5,如果想輸出結果更友好點,一層輸出一行, 可以改進一下,搞一個Map<Node, Integer> 記錄每個節點所在的層

static void levelOrder2(TreeNode n) {
    if (n == null) {
        return;
    }
    int currLevel = 1;

    Queue<TreeNode> queue = new LinkedList<>();
    queue.add(n);

    //弄1個map,記錄每個元素所在的層
    Map<TreeNode, Integer> levelMap = new HashMap<>();
    levelMap.put(n, 1);
    while (!queue.isEmpty()) {
        TreeNode node = queue.poll();
        //從map取查找出隊元素所在的層
        int nodeLevel = levelMap.get(node);
        //如果與當前層不一樣,說明來到了下一層(關鍵!)
        if (currLevel != nodeLevel) {
            currLevel += 1;
            //輸出換行符
            System.out.println();
        }
        System.out.print(node.val + " ");
        if (node.left != null) {
            //左節點入隊,說明取到了下層,把下層元素提前放入map
            levelMap.put(node.left, currLevel + 1);
            queue.add(node.left);
        }
        if (node.right != null) {
            //右節點入隊,說明取到了下層,把下層元素提前放入map
            levelMap.put(node.right, currLevel + 1);
            queue.add(node.right);
        }
    }
}

輸出為:

1
2 3 
4 5 

這個版本還可以繼續優化, 仔細想想, 其實只需要知道什么時候進入下一層就可以了, 沒必要搞個Map記錄所有節點在第幾層, 按頭-左-右的順序層層入隊, 然后不斷出隊, queue中同時最多也只會有3個元素.

    static void levelOrder3(TreeNode n) {
        if (n == null) {
            return;
        }
        //curEnd:本層最后1個節點
        //nextEnd:下層最后1個節點
        TreeNode curEnd = n, nextEnd = null;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(n);
        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();
            System.out.printf(node.val + " ");
            //逐層入隊
            //注:queue中,最多只會有頭-左-右 3個節點
            //入隊過程中,nextEnd最終肯定會指向本層最后1個節點
            if (node.left != null) {
                queue.add(node.left);
                nextEnd = node.left;
            }
            if (node.right != null) {
                queue.add(node.right);
                nextEnd = node.right;
            }
            if (node == curEnd) {
                //如果出隊的元素, 已經是本層最后1個,說明這層到頭了
                System.out.printf("\n");
                //進入下一層后,重新標識curEnd
                curEnd = nextEnd;
            }
        }
    }

輸出效果不變, 層序遍歷, 可以演化出很多面試題, 比如:
怎么打印出一顆二叉樹每層的序號, 每層最后1個節點的值 , 每層的節點數, 以及整顆樹的最大寬度?
無非就是在剛才這個版本上, 再加幾個變量, 統計一下而已.

/**
     * 打印每層的 層數,本層最后1個節點值,本層節點數, 以及最大寬度
     *
     * @param n
     */
    static void printLevelInfo(TreeNode n) {
        if (n == null) {
            return;
        }
        TreeNode curEnd = n, nextEnd = null;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(n);
        int currLevel = 1, currLevelNodes = 0, maxLevelNodes = 0;
        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();
            currLevelNodes++;
            if (node.left != null) {
                queue.add(node.left);
                nextEnd = node.left;
            }
            if (node.right != null) {
                queue.add(node.right);
                nextEnd = node.right;
            }
            if (node.equals(curEnd)) {
                System.out.println("level:" + currLevel + ",lastNode:" + curEnd.val + ",levelNodes:" + currLevelNodes);
                currLevel++;
                curEnd = nextEnd;
                maxLevelNodes = Math.max(currLevelNodes, maxLevelNodes);
                currLevelNodes = 0;
            }
        }
        maxLevelNodes = Math.max(currLevelNodes, maxLevelNodes);
        System.out.printf("maxLevelNodes:" + maxLevelNodes);
    }
 
再比如:如何判斷一顆樹是完全二叉樹?
分析:完全二叉樹的特點,除最后一層外,其它各層都是滿的,且最后一層如果出現未滿的情況,葉節點只能在左邊,即只能空出右節點的位置。
/**
     * 判斷是否完全二叉樹(complete binary tree)
     *
     * @param n
     */
    static boolean isCBT(TreeNode n) {
        if (n == null) {
            return true;
        }
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(n);

        //標記是否出現過,僅左孩子的情況
        boolean onlyLeftChild = false;

        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();

            TreeNode left = node.left;
            TreeNode right = node.right;

            //核心判斷
            if (
                //有右無左的情況,非完全二叉樹
                    (right != null && left == null)
                            ||
                            (
                                    //如果已經遇到過僅左孩子的情況, 后面必須都是葉節點
                                    onlyLeftChild && (right != null || left != null)
                            )

            ) {
                return false;
            }

            if (left != null) {
                queue.add(left);
            }
            if (right != null) {
                queue.add(right);
            }
            if (left != null && right == null) {
                //標識遇到只有子孩子的情況
                onlyLeftChild = true;
            }
        }
        return true;
    }

 

繼續:如何獲取二叉樹中,每個子節點到根節點的路徑?

比如這顆樹,每個子節點到根的路徑為:

4->2->1

5->2->1

6->3->1

7->3->1

2->1

3->1

同樣,還是在層次遍歷的基本上, 加2個map即可:

/**
     * 獲取每個節點到根節點的全路徑
     * @param node
     * @return
     */
    public static Map<TreeNode, List<TreeNode>> getToRootPath(TreeNode node) {
        if (node == null) {
            return null;
        }
        //記錄每個節點->父節點的1:1映射
        Map<TreeNode, TreeNode> parentMap = new HashMap<>();

        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(node);
        parentMap.put(node, null);
        while (!queue.isEmpty()) {
            TreeNode n = queue.poll();
            if (n.left != null) {
                queue.add(n.left);
                parentMap.put(n.left, n);
            }
            if (n.right != null) {
                queue.add(n.right);
                parentMap.put(n.right, n);
            }
        }

        //根據parentMap,整理出完整的到根節點的全路徑
        Map<TreeNode, List<TreeNode>> result = new HashMap<>();
        for (Map.Entry<TreeNode, TreeNode> entry : parentMap.entrySet()) {
            TreeNode self = entry.getKey();
            TreeNode parent = entry.getValue();

            //把當前節點,先保護起來
            TreeNode temp = self;
            List<TreeNode> path = new ArrayList<>();
            while (parent != null) {
                //輔助輸出
                System.out.printf(self.val + "->");
                path.add(self);
                self = parent;
                parent = parentMap.get(self);
                if (parent == null) {
                    //輔助輸出
                    System.out.printf(self.val + "\n");
                    path.add(self);
                }
            }
            result.put(temp, path);
        }
        return result;
    }

輸出:

3->1
4->2->1
5->2->1
2->1
6->3->1
7->3->1
{3=[3, 1], 4=[4, 2, 1], 1=[], 5=[5, 2, 1], 2=[2, 1], 6=[6, 3, 1], 7=[7, 3, 1]}

  

最后貼一個左神給的福利函數, 直觀的打印一顆樹
    /**
     * 直觀的打印一顆二叉樹
     *
     * @param n      節點
     * @param height 節點所在層數(注:根節點層數為0)
     * @param to     節點特征(H表示根節點, △表示父節點在左上方, ▽表示父節點在左下方)
     * @param len    節點打印時的最大寬度(手動指定)
     */
    static void printTree(TreeNode n, int height, String to, int len) {
        if (n == null) {
            return;
        }
        printTree(n.right, height + 1, "▽", len);
        String val = to + n.val + to;
        int lenV = val.length();
        int lenL = (len - lenV) / 2;
        int lenR = len - lenV - lenL;
        val = getSpace(lenL) + val + getSpace(lenR);
        System.out.println(getSpace(height * len) + val);
        printTree(n.left, height + 1, "△", len);
    }

    static String getSpace(int num) {
        String space = " ";
        StringBuilder buf = new StringBuilder();
        for (int i = 0; i < num; i++) {
            buf.append(space);
        }
        return buf.toString();
    }

用法示例:

    static TreeNode init() {
        TreeNode n1 = new TreeNode(4);
        TreeNode n2_1 = new TreeNode(2);
        TreeNode n2_2 = new TreeNode(6);
        TreeNode n3_1 = new TreeNode(1);
        TreeNode n3_2 = new TreeNode(3);
        TreeNode n3_3 = new TreeNode(5);
        TreeNode n3_4 = new TreeNode(7);
        n1.left = n2_1;
        n1.right = n2_2;
        n2_1.left = n3_1;
        n2_1.right = n3_2;
        n2_2.left = n3_3;
        n2_2.right = n3_4;
        return n1;
    }

    public static void main(String[] args) {
        TreeNode root = init();
        printTree(root, 0, "H", 10);
    }

輸出:

                       ▽7▽    
             ▽6▽    
                       △5△    
   H4H    
                       ▽3▽    
             △2△    
                       △1△ 

把頭側過來看, 就是一顆樹


免責聲明!

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



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