平時無論是工作還是學習中,在寫代碼時,樹總是一個非常常見的數據結構。在我們完成一棵樹的構建之后,如果我們想要看這棵樹的結構,不像數組或者List等數據結構,我們可以非常方便地用各種方式將其中的所有元素打印出來,對於樹而言,這個過程要麻煩得多,我們可以用各種遍歷方式得到這棵樹的結構,但是終究還是不夠直觀。
不知大家有沒有想過,如果我們可以按照樹的結構,將其打印出來就好了,那么本文就是一種實現這個目標的思路以供參考。
引言
樹的結構
在本文中所用的樹的結構是leetcode上所用的樹的結構,其定義如下:
public class TreeNode {
public int val;
public TreeNode left;
public TreeNode right;
public TreeNode(int x) { val = x; }
}
如何方便地創建一個樹的數據結構?
在下面貼的這篇博客中中有詳細的講解,基於Leetcode中樹的編碼方式,由一個數組直接得到一棵樹。
如何打印一棵樹
在這里,我的總體思路是,用一個二維的字符串數組來儲存每個位置應該打印什么樣的輸出。
首先,先確定樹的形狀。為了美觀,我設定在最后一行的每個數字之間的間隔為3個空格,而在之上的每一層的間隔,有興趣的同學可以自己推算一下,總之,越往上,間隔是越大的,而且是一個簡單的線性增加的關系。
為了繪制出這樣的形狀,首先,我們需要獲得樹的層數(用一個簡單的遞歸即可得到),根據樹的層數,確定我們的二維數組的大小,即高度和寬度。之后,用先序遍歷的方式,遍歷樹的每個節點,並進行相對應的寫入操作。
更詳細的解釋可以看代碼中的注釋,當然,也可以根據下面TreeOperationTest.java中的demo直接在自己的代碼中調用這個方法來檢查自己的樹。
話不多說,直接貼上所用的代碼:
// TreeOperation.java
public class TreeOperation {
/* 樹的結構示例: 1 / \ 2 3 / \ / \ 4 5 6 7 */
// 用於獲得樹的層數
public static int getTreeDepth(TreeNode root) {
return root == null ? 0 : (1 + Math.max(getTreeDepth(root.left), getTreeDepth(root.right)));
}
private static void writeArray(TreeNode currNode, int rowIndex, int columnIndex, String[][] res, int treeDepth) {
// 保證輸入的樹不為空
if (currNode == null) return;
// 先將當前節點保存到二維數組中
res[rowIndex][columnIndex] = String.valueOf(currNode.val);
// 計算當前位於樹的第幾層
int currLevel = ((rowIndex + 1) / 2);
// 若到了最后一層,則返回
if (currLevel == treeDepth) return;
// 計算當前行到下一行,每個元素之間的間隔(下一行的列索引與當前元素的列索引之間的間隔)
int gap = treeDepth - currLevel - 1;
// 對左兒子進行判斷,若有左兒子,則記錄相應的"/"與左兒子的值
if (currNode.left != null) {
res[rowIndex + 1][columnIndex - gap] = "/";
writeArray(currNode.left, rowIndex + 2, columnIndex - gap * 2, res, treeDepth);
}
// 對右兒子進行判斷,若有右兒子,則記錄相應的"\"與右兒子的值
if (currNode.right != null) {
res[rowIndex + 1][columnIndex + gap] = "\\";
writeArray(currNode.right, rowIndex + 2, columnIndex + gap * 2, res, treeDepth);
}
}
public static void show(TreeNode root) {
if (root == null) System.out.println("EMPTY!");
// 得到樹的深度
int treeDepth = getTreeDepth(root);
// 最后一行的寬度為2的(n - 1)次方乘3,再加1
// 作為整個二維數組的寬度
int arrayHeight = treeDepth * 2 - 1;
int arrayWidth = (2 << (treeDepth - 2)) * 3 + 1;
// 用一個字符串數組來存儲每個位置應顯示的元素
String[][] res = new String[arrayHeight][arrayWidth];
// 對數組進行初始化,默認為一個空格
for (int i = 0; i < arrayHeight; i ++) {
for (int j = 0; j < arrayWidth; j ++) {
res[i][j] = " ";
}
}
// 從根節點開始,遞歸處理整個樹
// res[0][(arrayWidth + 1)/ 2] = (char)(root.val + '0');
writeArray(root, 0, arrayWidth/ 2, res, treeDepth);
// 此時,已經將所有需要顯示的元素儲存到了二維數組中,將其拼接並打印即可
for (String[] line: res) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < line.length; i ++) {
sb.append(line[i]);
if (line[i].length() > 1 && i <= line.length - 1) {
i += line[i].length() > 4 ? 2: line[i].length() - 1;
}
}
System.out.println(sb.toString());
}
}
}
接下來,是用於測試的程序,如果需要調用上面的方法,以下面的代碼為demo即可。需要注意的一點是,其中構建樹的代碼為如何創建一棵樹中的方法,需要將這些.java文件放在個包中(包括TreeNode.java),才可以正常使用。
// // TreeOperationTest.java
public class TreeOperatinTest {
public static void main(String[] args) {
// 根據給定的數組創建一棵樹
TreeNode root = ConstructTree.constructTree(new Integer[] {1, 2, 3, 4, 5 ,6, 7});
// 將剛剛創建的樹打印出來
TreeOperation.show(root);
}
}
輸出:
1
/ \
2 3
/ \ / \
4 5 6 7
可以看到,這里我們用了兩行代碼,便創建出了一棵我們需要的樹,並且按照樹的格式將一棵完整的樹打印了出來。
當然,我們也可以換一棵樹再來測試,我們就使用在這篇如何創建一棵樹中的曾經使用過的例子再進行一次測試:
public class TreeOperatinTest {
public static void main(String[] args) {
TreeNode root = ConstructTree.constructTree(new Integer[] {5,4,8,11,null,13,4,7,2,null,null,null,1});
TreeOperation.show(root);
}
}
輸出:
5
/ \
4 8
/ / \
11 13 4
/ \ \
7 2 1
可以看到,即使樹並不完整, 且其中有超過1位的數字,依然可以正確地輸出。
一點問題
由於本方法的思路是基於字符串的數組的,所以並不可能完美適配所有情況,比如當樹的高度很高以后,可能看起來會很奇怪(😆)。
還有一個問題就是,雖然已經做了自適應處理,但是,如果出現超過5位的數字(比如123123),其所在的行可能會有一點向右的偏移,若偏的不多,是不影響觀察的,但若偏的多了就。。。。不過這里已經做了處理,所以出現三位或者四位數的時候是沒有問題的。
不過,在日常的應用中,應該是完全夠用的,希望這段代碼能為大家帶來便利。
