1. 简介
打印是数据结构可视化中最常见的手段之一。但对于树结构来说,由于其天然的层次性,直接用文本打印往往容易“翻车”——结构混乱、对不齐、看着费劲。
本文将带你实现一个简洁实用的二叉树打印工具,方便调试和观察树形结构。我们聚焦于 水平展开式树图(horizontal tree),这种形式更贴近文本流方向,实现简单且可读性强。
2. 树形图的几种表现形式
虽然控制台只能靠字符拼图,但树的展示方式五花八门。选择哪种,主要看树的大小和是否平衡。
比如下面这种紧凑竖排式:
但我们不选它,原因很现实:
- 节点值一长就错位
- 深度一深就挤成一团
- 实现起来绕脑
我们选的是 水平展开式树图,长这样:
✅ 优势很明显:
- 适合展示深、宽、不平衡的树
- 节点值长度不影响整体结构
- 实现逻辑清晰,不容易踩坑
接下来我们就基于这种风格,一步步实现一个 BinaryTreePrinter
。
3. 二叉树模型定义
先定义一个极简的二叉树节点类,不搞花里胡哨,就三个字段:值、左子、右子。
public class BinaryTreeModel {
private Object value;
private BinaryTreeModel left;
private BinaryTreeModel right;
public BinaryTreeModel(Object value) {
this.value = value;
}
// standard getters and setters
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
public BinaryTreeModel getLeft() {
return left;
}
public void setLeft(BinaryTreeModel left) {
this.left = left;
}
public BinaryTreeModel getRight() {
return right;
}
public void setRight(BinaryTreeModel right) {
this.right = right;
}
}
⚠️ 注意:这里用 Object
是为了通用性,实际项目中可以用泛型。
4. 测试数据准备
动手之前,先构造一棵测试树,结构如下:
root
/ \
node1 node2
/ \ / \
node3 node4 node5 node6
/
node7
/ \
node8 node9
对应代码:
BinaryTreeModel root = new BinaryTreeModel("root");
BinaryTreeModel node1 = new BinaryTreeModel("node1");
BinaryTreeModel node2 = new BinaryTreeModel("node2");
root.setLeft(node1);
root.setRight(node2);
BinaryTreeModel node3 = new BinaryTreeModel("node3");
BinaryTreeModel node4 = new BinaryTreeModel("node4");
node1.setLeft(node3);
node1.setRight(node4);
node2.setLeft(new BinaryTreeModel("node5"));
node2.setRight(new BinaryTreeModel("node6"));
BinaryTreeModel node7 = new BinaryTreeModel("node7");
node3.setLeft(node7);
node7.setLeft(new BinaryTreeModel("node8"));
node7.setRight(new BinaryTreeModel("node9"));
这棵树有深度、有分支,够我们验证打印逻辑。
5. 二叉树打印器实现
按单一职责原则,打印逻辑不该塞进 BinaryTreeModel
。虽然可以用访问者模式解耦,但这里为了简洁,我们单独搞个 BinaryTreePrinter
类,直接传入根节点搞定。
5.1 初版:前序遍历打底
最简单的思路:前序遍历,逐行输出节点值。
public void traversePreOrder(StringBuilder sb, BinaryTreeModel node) {
if (node != null) {
sb.append(node.getValue());
sb.append("\n");
traversePreOrder(sb, node.getLeft());
traversePreOrder(sb, node.getRight());
}
}
public void print(PrintStream os) {
StringBuilder sb = new StringBuilder();
traversePreOrder(sb, this.tree);
os.print(sb.toString());
}
测试调用:
new BinaryTreePrinter(root).print(System.out);
输出结果:
root
node1
node3
node7
node8
node9
node4
node2
node5
node6
❌ 问题:这只是节点列表,根本看不出树形结构。下一步加“连接线”。
5.2 加上树形连接线
用三个字符画出层次感:
├──
:有右兄弟的左子节点└──
:最后一个子节点(无右兄弟)│
:垂直连接线,表示上层还有兄弟
更新遍历方法,加入缩进(padding)和指针(pointer):
public void traversePreOrder(StringBuilder sb, String padding, String pointer, BinaryTreeModel node) {
if (node != null) {
sb.append(padding);
sb.append(pointer);
sb.append(node.getValue());
sb.append("\n");
StringBuilder paddingBuilder = new StringBuilder(padding);
paddingBuilder.append("│ ");
String paddingForBoth = paddingBuilder.toString();
String pointerForRight = "└──";
String pointerForLeft = (node.getRight() != null) ? "├──" : "└──";
traversePreOrder(sb, paddingForBoth, pointerForLeft, node.getLeft());
traversePreOrder(sb, paddingForBoth, pointerForRight, node.getRight());
}
}
print
方法也同步更新:
public void print(PrintStream os) {
StringBuilder sb = new StringBuilder();
traversePreOrder(sb, "", "", this.tree);
os.print(sb.toString());
}
此时输出效果:
✅ 初步成型,但还有三处“多余竖线”需要干掉:
- 根节点下的多余
│
- 右子树下的多余
│
- 无右兄弟的左子树下的多余
│
5.3 优化:区分根节点与子节点处理
问题根源:根节点没有父辈,不该画 │
;末尾分支也不该延续 │
。
解决方案:拆分逻辑,根节点单独处理,子节点递归时传入是否有右兄弟(hasRightSibling
)。
重写核心方法:
public String traversePreOrder(BinaryTreeModel root) {
if (root == null) {
return "";
}
StringBuilder sb = new StringBuilder();
sb.append(root.getValue());
String pointerRight = "└──";
String pointerLeft = (root.getRight() != null) ? "├──" : "└──";
traverseNodes(sb, "", pointerLeft, root.getLeft(), root.getRight() != null);
traverseNodes(sb, "", pointerRight, root.getRight(), false);
return sb.toString();
}
子节点递归方法:
public void traverseNodes(StringBuilder sb, String padding, String pointer, BinaryTreeModel node,
boolean hasRightSibling) {
if (node != null) {
sb.append("\n");
sb.append(padding);
sb.append(pointer);
sb.append(node.getValue());
StringBuilder paddingBuilder = new StringBuilder(padding);
if (hasRightSibling) {
paddingBuilder.append("│ ");
} else {
paddingBuilder.append(" ");
}
String paddingForBoth = paddingBuilder.toString();
String pointerRight = "└──";
String pointerLeft = (node.getRight() != null) ? "├──" : "└──";
traverseNodes(sb, paddingForBoth, pointerLeft, node.getLeft(), node.getRight() != null);
traverseNodes(sb, paddingForBoth, pointerRight, node.getRight(), false);
}
}
print
方法简化为:
public void print(PrintStream os) {
os.print(traversePreOrder(tree));
}
最终输出,清爽无多余线条:
6. 总结
本文实现了一个简单粗暴但非常实用的二叉树打印器,核心思路:
- 采用 水平展开式布局,符合文本流习惯
- 通过
├──
、└──
、│
三字符构建树形连接线 - 区分根节点与子节点处理,避免多余竖线
- 递归中传递
hasRightSibling
控制缩进样式
这个工具在调试 AVL、红黑树、B+树等复杂结构时特别有用,建议集合。代码已完整给出,直接复制就能用。