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