1. 简介

打印是数据结构可视化中最常见的手段之一。但对于树结构来说,由于其天然的层次性,直接用文本打印往往容易“翻车”——结构混乱、对不齐、看着费劲。

本文将带你实现一个简洁实用的二叉树打印工具,方便调试和观察树形结构。我们聚焦于 水平展开式树图(horizontal tree),这种形式更贴近文本流方向,实现简单且可读性强。

2. 树形图的几种表现形式

虽然控制台只能靠字符拼图,但树的展示方式五花八门。选择哪种,主要看树的大小和是否平衡。

比如下面这种紧凑竖排式:

tree diagram 1

但我们不选它,原因很现实:

  • 节点值一长就错位
  • 深度一深就挤成一团
  • 实现起来绕脑

我们选的是 水平展开式树图,长这样:

tree diagram 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());
}

此时输出效果:

tree diagram 3

✅ 初步成型,但还有三处“多余竖线”需要干掉:

  1. 根节点下的多余
  2. 右子树下的多余
  3. 无右兄弟的左子树下的多余

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));
}

最终输出,清爽无多余线条:

tree diagram 5

6. 总结

本文实现了一个简单粗暴但非常实用的二叉树打印器,核心思路:

  • 采用 水平展开式布局,符合文本流习惯
  • 通过 ├──└── 三字符构建树形连接线
  • 区分根节点与子节点处理,避免多余竖线
  • 递归中传递 hasRightSibling 控制缩进样式

这个工具在调试 AVL、红黑树、B+树等复杂结构时特别有用,建议集合。代码已完整给出,直接复制就能用。


原始标题:How to Print a Binary Tree Diagram | Baeldung