二叉樹的節點結構如下:
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△
把頭側過來看, 就是一顆樹