Skip to content

前言

更新: 10/24/2025 字数: 0 字 时长: 0 分钟

1.工具类

1.1. BinaryTreePrinter.java

java
import java.util.*;

/**
 * 让开发者能够直观地看到树的结构,比如二叉树、红黑树等。
 * <p>
 * 主要特点:
 * 1. 使用函数式接口,不需要类实现特定接口
 * 2. 支持自定义节点值的显示格式
 * 3. 支持高亮显示特殊节点(比如红黑树中的红色节点)
 * 4. 提供简单易用的静态方法和灵活的Builder模式
 *
 * @author yolk101336
 * @since 2025-10-05
 */
public class BinaryTreePrinter {

    // ========== 打印用的字符常量 ==========

    /**
     * 水平间距字符 - 用制表符来保证对齐
     */
    private static final String HORIZONTAL_SPACING = "\t";
    /**
     * 分支连接线 - 连接父子节点的横线
     */
    private static final String BRANCH_CONNECTOR = "____";
    /**
     * 垂直连接线 - 表示节点位置的竖线
     */
    private static final String VERTICAL_CONNECTOR = "|";
    /**
     * ANSI颜色代码 - 高亮开始(红色粗体)
     */
    private static final String HIGHLIGHT_START = "\033[91;1m";
    /**
     * ANSI颜色代码 - 高亮结束(重置颜色)
     */
    private static final String HIGHLIGHT_END = "\033[0m";

    // ========== 函数式接口定义 ==========

    /**
     * 根节点提供者接口
     * <p>
     * 这个接口用来获取树的根节点。
     * 实现者需要返回代表根节点的对象,如果树为空则返回null。
     */
    @FunctionalInterface
    public interface NodeProvider {
        /**
         * 获取树的根节点
         *
         * @return 根节点对象,如果树为空返回null
         */
        Object getRoot();
    }

    /**
     * 子节点提供者接口
     * <p>
     * 这个接口用来获取指定节点的左子节点或右子节点。
     * 实现者需要根据isLeft参数决定返回左子节点还是右子节点。
     */
    @FunctionalInterface
    public interface ChildProvider {
        /**
         * 获取指定节点的子节点
         *
         * @param node   父节点
         * @param isLeft true表示获取左子节点,false表示获取右子节点
         * @return 子节点对象,如果不存在返回null
         */
        Object getChild(Object node, boolean isLeft);
    }

    /**
     * 节点值提供者接口
     * <p>
     * 这个接口用来获取节点要显示的值。
     * 实现者可以自定义节点值的格式化方式。
     *
     * @param <T> 节点值的类型
     */
    @FunctionalInterface
    public interface ValueProvider<T> {
        /**
         * 获取节点的显示值
         *
         * @param node 要获取值的节点
         * @return 节点的显示值
         */
        T getValue(Object node);
    }

    /**
     * 节点高亮提供者接口
     * <p>
     * 这个接口用来判断某个节点是否需要高亮显示。
     * 比如在红黑树中,红色节点可能需要用不同颜色显示。
     */
    @FunctionalInterface
    public interface HighlightProvider {
        /**
         * 判断节点是否需要高亮显示
         *
         * @param node 要判断的节点
         * @return true表示需要高亮,false表示正常显示
         */
        boolean isHighlighted(Object node);
    }

    // ========== Builder模式配置类 ==========

    /**
     * 树打印配置构建器
     * <p>
     * 这个类使用Builder模式,让用户可以逐步配置树的打印参数。
     * 使用方式:
     * 1. 调用TreePrintBuilder.create()创建构建器
     * 2. 链式调用withXxx()方法设置各种参数
     * 3. 最后调用build()生成打印结果
     *
     * @param <T> 节点值的类型
     */
    public static class TreePrintBuilder<T> {
        /**
         * 根节点提供者 - 必须设置
         */
        private NodeProvider rootProvider;
        /**
         * 子节点提供者 - 必须设置
         */
        private ChildProvider childProvider;
        /**
         * 节点值提供者 - 必须设置
         */
        private ValueProvider<T> valueProvider;
        /**
         * 节点高亮提供者 - 可选,默认所有节点都不高亮
         */
        private HighlightProvider highlightProvider = node -> false;

        /**
         * 私有构造函数,防止直接new,必须通过create()方法创建
         */
        private TreePrintBuilder() {
        }

        /**
         * 静态工厂方法 - 创建一个新的构建器实例
         * <p>
         * 这是创建TreePrintBuilder的唯一方式,采用了工厂模式。
         *
         * @param <T> 节点值的类型
         * @return 新的构建器实例
         */
        public static <T> TreePrintBuilder<T> create() {
            return new TreePrintBuilder<>();
        }

        /**
         * 设置根节点提供者
         *
         * @param rootProvider 根节点提供者,不能为null
         * @return 当前构建器实例,支持链式调用
         */
        public TreePrintBuilder<T> withRoot(NodeProvider rootProvider) {
            this.rootProvider = rootProvider;
            return this;
        }

        /**
         * 设置子节点提供者
         *
         * @param childProvider 子节点提供者,不能为null
         * @return 当前构建器实例,支持链式调用
         */
        public TreePrintBuilder<T> withChildren(ChildProvider childProvider) {
            this.childProvider = childProvider;
            return this;
        }

        /**
         * 设置节点值提供者
         *
         * @param valueProvider 节点值提供者,不能为null
         * @return 当前构建器实例,支持链式调用
         */
        public TreePrintBuilder<T> withValues(ValueProvider<T> valueProvider) {
            this.valueProvider = valueProvider;
            return this;
        }

        /**
         * 设置节点高亮提供者(可选)
         *
         * @param highlightProvider 节点高亮提供者
         * @return 当前构建器实例,支持链式调用
         */
        public TreePrintBuilder<T> withHighlighting(HighlightProvider highlightProvider) {
            this.highlightProvider = highlightProvider;
            return this;
        }

        /**
         * 构建并生成树的字符串表示
         * <p>
         * 这个方法会检查必需的参数是否都已设置,然后创建树渲染器并生成结果。
         *
         * @return 树的ASCII艺术字符串表示
         * @throws NullPointerException 如果必需的参数没有设置
         */
        public String build() {
            // 检查必需参数 - 这三个参数是必须的,否则无法渲染树
            Objects.requireNonNull(rootProvider, "Root provider is required");
            Objects.requireNonNull(childProvider, "Child provider is required");
            Objects.requireNonNull(valueProvider, "Value provider is required");

            // 创建函数式树渲染器
            TreeRenderer<T> renderer = new FunctionalTreeRenderer<>(
                    rootProvider, childProvider, valueProvider, highlightProvider);
            // 创建树打印器并生成结果
            return new BinaryTreePrinter(renderer).generateTreeString();
        }
    }

    /**
     * 向后兼容的静态工厂方法
     * <p>
     * 这个方法是为了保持API的一致性,实际上调用的是TreePrintBuilder.create()。
     *
     * @param <T> 节点值类型
     * @return 新的构建器实例
     */
    public static <T> TreePrintBuilder<T> create() {
        return TreePrintBuilder.create();
    }

    // ========== 私有实现部分 ==========

    /**
     * 节点布局信息映射表 - 存储每个节点在水平方向上的位置信息
     */
    private final Map<Object, NodeLayoutInfo> nodeLayoutMap = new HashMap<>();
    /**
     * 树渲染器 - 提供树结构信息的接口
     */
    private final TreeRenderer<?> treeRenderer;

    /**
     * 私有构造函数
     * <p>
     * TreePrinter只能通过静态方法创建,这是为了保持API的简洁性。
     *
     * @param treeRenderer 树渲染器
     */
    private BinaryTreePrinter(TreeRenderer<?> treeRenderer) {
        this.treeRenderer = treeRenderer;
    }

    /**
     * 函数式树渲染器实现类
     * <p>
     * 这个类将用户提供的函数式接口适配到内部的TreeRenderer接口。
     * 它是一个适配器模式的实现。
     *
     * @param <T> 节点值类型
     */
    private static class FunctionalTreeRenderer<T> implements TreeRenderer<T> {
        /**
         * 根节点提供者
         */
        private final NodeProvider rootProvider;
        /**
         * 子节点提供者
         */
        private final ChildProvider childProvider;
        /**
         * 节点值提供者
         */
        private final ValueProvider<T> valueProvider;
        /**
         * 节点高亮提供者
         */
        private final HighlightProvider highlightProvider;

        /**
         * 构造函数
         *
         * @param rootProvider      根节点提供者
         * @param childProvider     子节点提供者
         * @param valueProvider     节点值提供者
         * @param highlightProvider 节点高亮提供者
         */
        FunctionalTreeRenderer(NodeProvider rootProvider, ChildProvider childProvider,
                               ValueProvider<T> valueProvider, HighlightProvider highlightProvider) {
            this.rootProvider = rootProvider;
            this.childProvider = childProvider;
            this.valueProvider = valueProvider;
            this.highlightProvider = highlightProvider;
        }

        @Override
        public Object getRoot() {
            return rootProvider.getRoot();
        }

        @Override
        public Object getLeftChild(Object node) {
            return childProvider.getChild(node, true);
        }

        @Override
        public Object getRightChild(Object node) {
            return childProvider.getChild(node, false);
        }

        @Override
        public T getNodeValue(Object node) {
            return valueProvider.getValue(node);
        }

        @Override
        public boolean isHighlighted(Object node) {
            return highlightProvider.isHighlighted(node);
        }
    }

    /**
     * 内部树渲染器接口
     * <p>
     * 这个接口定义了渲染树所需的基本操作。
     * 它是包私有的,外部用户无法直接使用,保证了封装性。
     *
     * @param <T> 节点值类型
     */
    interface TreeRenderer<T> {
        /**
         * 获取根节点
         */
        Object getRoot();

        /**
         * 获取左子节点
         */
        Object getLeftChild(Object node);

        /**
         * 获取右子节点
         */
        Object getRightChild(Object node);

        /**
         * 获取节点值
         */
        T getNodeValue(Object node);

        /**
         * 判断节点是否高亮
         */
        boolean isHighlighted(Object node);
    }

    /**
     * 节点布局信息类
     * <p>
     * 这个类存储节点在最终输出中的位置信息,
     * 用于计算节点之间的间距和连接线的位置。
     */
    private static class NodeLayoutInfo {
        /**
         * 节点在水平方向上的索引位置
         */
        final int horizontalIndex;
        /**
         * 节点值显示所需的宽度
         */
        final int displayWidth;

        /**
         * 构造函数
         *
         * @param horizontalIndex 水平索引
         * @param displayWidth    显示宽度
         */
        NodeLayoutInfo(int horizontalIndex, int displayWidth) {
            this.horizontalIndex = horizontalIndex;
            this.displayWidth = displayWidth;
        }
    }

    /**
     * 层级渲染数据类
     * <p>
     * 这个类用于存储渲染单个层级时的临时数据。
     * 每个层级需要渲染三行:上连接线、节点值行、下连接线。
     */
    private static class LevelRenderData {
        /**
         * 上层连接线 - 显示从父节点到当前节点的连接
         */
        StringBuilder topConnectorLine = new StringBuilder();
        /**
         * 节点值行 - 显示当前层级所有节点的值
         */
        StringBuilder nodeValueLine = new StringBuilder();
        /**
         * 下层连接线 - 显示从当前节点到子节点的连接
         */
        StringBuilder bottomConnectorLine = new StringBuilder();
        /**
         * 前一个处理的节点 - 用于计算间距
         */
        Object previousProcessedNode;
        /**
         * 最后一个右节点 - 用于连接线的计算
         */
        Object lastRightmostNode;

        /**
         * 将三行内容组合成字符串
         *
         * @return 当前层级的完整渲染结果
         */
        @Override
        public String toString() {
            return topConnectorLine + "\n" + nodeValueLine + "\n" + bottomConnectorLine + "\n";
        }
    }

    /**
     * 生成树的完整字符串表示
     * <p>
     * 这是整个打印过程的入口方法,它协调各个步骤来生成最终结果。
     *
     * @return 树的ASCII艺术字符串
     */
    public String generateTreeString() {
        Object root = treeRenderer.getRoot();
        if (root == null) {
            return "Empty Tree";
        }

        // 第一步:计算所有节点的布局位置
        calculateNodeLayout();
        // 第二步:按层级渲染树结构
        return renderLevelByLevel(Collections.singletonList(root));
    }

    /**
     * 计算节点布局
     * <p>
     * 这个方法通过中序遍历确定节点的水平位置。
     * 中序遍历可以保证左子节点在左边,右子节点在右边,
     * 这样渲染出的树结构看起来更自然。
     */
    private void calculateNodeLayout() {
        // 存储中序遍历的节点顺序
        List<Object> inOrderNodes = new ArrayList<>();
        // 执行中序遍历
        performInOrderTraversal(treeRenderer.getRoot(), inOrderNodes);

        // 累计宽度偏移 - 用于处理不同宽度的节点值
        int cumulativeWidthOffset = 0;
        for (int i = 0; i < inOrderNodes.size(); i++) {
            Object node = inOrderNodes.get(i);
            // 计算节点值的显示宽度(以4个字符为单位进行归一化)
            int nodeDisplayWidth = treeRenderer.getNodeValue(node).toString().length() / 4;
            // 存储节点的布局信息
            nodeLayoutMap.put(node, new NodeLayoutInfo(i + cumulativeWidthOffset, nodeDisplayWidth));
            // 更新累计偏移
            cumulativeWidthOffset += nodeDisplayWidth;
        }
    }

    /**
     * 执行中序遍历
     * <p>
     * 中序遍历的顺序是:左子树 -> 根节点 -> 右子树
     * 这样遍历出的节点顺序在水平方向上是从左到右的。
     *
     * @param node     当前节点
     * @param nodeList 存储遍历结果的列表
     */
    private void performInOrderTraversal(Object node, List<Object> nodeList) {
        if (node == null) return;

        // 递归遍历左子树
        performInOrderTraversal(treeRenderer.getLeftChild(node), nodeList);
        // 访问当前节点
        nodeList.add(node);
        // 递归遍历右子树
        performInOrderTraversal(treeRenderer.getRightChild(node), nodeList);
    }

    /**
     * 按层级渲染树结构
     * <p>
     * 这个方法使用递归的方式,逐层渲染树结构。
     * 每次处理一个层级的所有节点,然后递归处理下一层级。
     *
     * @param currentLevelNodes 当前层级的所有节点
     * @return 当前层级及其以下所有层级的渲染结果
     */
    private String renderLevelByLevel(List<Object> currentLevelNodes) {
        // 递归终止条件:当前层级没有节点
        if (currentLevelNodes.isEmpty()) return "";

        // 存储下一层级的节点
        List<Object> nextLevelNodes = new ArrayList<>();
        // 当前层级的渲染数据
        LevelRenderData levelData = new LevelRenderData();

        // 处理当前层级的每个节点
        for (Object node : currentLevelNodes) {
            processNodeForRendering(node, levelData, nextLevelNodes);
        }

        // 递归渲染下一层级,并将结果拼接
        return levelData.toString() + renderLevelByLevel(nextLevelNodes);
    }

    /**
     * 处理单个节点的渲染
     * <p>
     * 这个方法负责渲染单个节点,包括:
     * 1. 计算节点的位置
     * 2. 绘制连接线
     * 3. 显示节点值
     * 4. 收集子节点供下一层级处理
     *
     * @param node           要处理的节点
     * @param levelData      当前层级的渲染数据
     * @param nextLevelNodes 下一层级的节点列表
     */
    private void processNodeForRendering(Object node, LevelRenderData levelData,
                                         List<Object> nextLevelNodes) {
        // 计算与前一个节点的间距
        int spacingBeforeConnector = calculateSpacingBeforeConnector(node, levelData.previousProcessedNode);
        int spacingBeforeValue = calculateSpacingBeforeValue(node, levelData.previousProcessedNode);

        // 构建上层连接线(垂直线,表示节点位置)
        levelData.topConnectorLine.append(generateSpacing(spacingBeforeConnector)).append(VERTICAL_CONNECTOR);
        // 构建节点值行
        levelData.nodeValueLine.append(generateSpacing(spacingBeforeValue)).append(formatNodeValue(node));

        // 如果是叶子节点,不需要绘制下层连接线
        if (isLeafNode(node)) {
            levelData.previousProcessedNode = node;
            return;
        }

        // 构建下层连接线并收集子节点
        String childConnectors = buildChildConnectors(node, nextLevelNodes);
        Object leftmostChild = getLeftmostDescendant(node);
        int connectorSpacing = calculateConnectorSpacing(leftmostChild, levelData.lastRightmostNode);

        levelData.bottomConnectorLine.append(generateSpacing(connectorSpacing)).append(childConnectors);
        // 更新状态,为处理下一个节点做准备
        levelData.previousProcessedNode = node;
        levelData.lastRightmostNode = getRightmostDescendant(node);
    }

    /**
     * 构建子节点连接线
     * <p>
     * 这个方法负责绘制从父节点到子节点的连接线。
     * 连接线的格式是:左横线 + 垂直线 + 右横线
     *
     * @param parentNode 父节点
     * @param childNodes 子节点列表(用于收集子节点)
     * @return 连接线字符串
     */
    private String buildChildConnectors(Object parentNode, List<Object> childNodes) {
        Object leftChild = treeRenderer.getLeftChild(parentNode);
        Object rightChild = treeRenderer.getRightChild(parentNode);
        String leftConnector = "";
        String rightConnector = "";

        // 处理左子节点
        if (leftChild != null) {
            childNodes.add(leftChild);
            // 计算从左子节点到父节点的横线长度
            leftConnector = generateHorizontalLine(getHorizontalIndex(parentNode) - getHorizontalIndex(leftChild));
        }

        // 处理右子节点
        if (rightChild != null) {
            childNodes.add(rightChild);
            // 计算从父节点到右子节点的横线长度
            rightConnector = generateHorizontalLine(getHorizontalIndex(rightChild) - getHorizontalIndex(parentNode));
        }

        // 组合连接线:左横线 + 中心垂直线 + 右横线
        return leftConnector + VERTICAL_CONNECTOR + rightConnector;
    }

    // ========== 工具方法 ==========

    /**
     * 获取节点的水平索引位置
     *
     * @param node 节点
     * @return 水平索引,如果节点为null则返回0
     */
    private int getHorizontalIndex(Object node) {
        return node == null ? 0 : nodeLayoutMap.get(node).horizontalIndex;
    }

    /**
     * 获取节点的显示宽度
     *
     * @param node 节点
     * @return 显示宽度,如果节点为null则返回0
     */
    private int getDisplayWidth(Object node) {
        return node == null ? 0 : nodeLayoutMap.get(node).displayWidth;
    }

    /**
     * 判断是否为叶子节点
     *
     * @param node 节点
     * @return true表示是叶子节点(没有子节点)
     */
    private boolean isLeafNode(Object node) {
        return treeRenderer.getLeftChild(node) == null && treeRenderer.getRightChild(node) == null;
    }

    /**
     * 获取最左边的后代节点
     * <p>
     * 如果有左子节点就返回左子节点,否则返回自己。
     * 这用于确定连接线的起始位置。
     *
     * @param node 节点
     * @return 最左边的后代节点
     */
    private Object getLeftmostDescendant(Object node) {
        return treeRenderer.getLeftChild(node) != null ? treeRenderer.getLeftChild(node) : node;
    }

    /**
     * 获取最右边的后代节点
     * <p>
     * 如果有右子节点就返回右子节点,否则返回自己。
     * 这用于确定连接线的结束位置。
     *
     * @param node 节点
     * @return 最右边的后代节点
     */
    private Object getRightmostDescendant(Object node) {
        return treeRenderer.getRightChild(node) != null ? treeRenderer.getRightChild(node) : node;
    }

    /**
     * 计算连接符之前的间距
     *
     * @param currentNode  当前节点
     * @param previousNode 前一个节点
     * @return 需要的间距数量
     */
    private int calculateSpacingBeforeConnector(Object currentNode, Object previousNode) {
        return getHorizontalIndex(currentNode) - getHorizontalIndex(previousNode);
    }

    /**
     * 计算节点值之前的间距
     * <p>
     * 这个计算需要考虑前一个节点的显示宽度。
     *
     * @param currentNode  当前节点
     * @param previousNode 前一个节点
     * @return 需要的间距数量
     */
    private int calculateSpacingBeforeValue(Object currentNode, Object previousNode) {
        return getHorizontalIndex(currentNode) - getHorizontalIndex(previousNode) - getDisplayWidth(previousNode);
    }

    /**
     * 计算连接器的间距
     *
     * @param leftmostChild 最左边的子节点
     * @param lastRightNode 最后一个右节点
     * @return 需要的间距数量
     */
    private int calculateConnectorSpacing(Object leftmostChild, Object lastRightNode) {
        return getHorizontalIndex(leftmostChild) - getHorizontalIndex(lastRightNode);
    }

    /**
     * 生成指定数量的空格字符串
     *
     * @param count 空格数量
     * @return 空格字符串,如果count <= 0则返回空字符串
     */
    private String generateSpacing(int count) {
        return count <= 0 ? "" : HORIZONTAL_SPACING.repeat(count);
    }

    /**
     * 生成指定长度的水平连接线
     *
     * @param length 连接线长度
     * @return 连接线字符串,如果length <= 0则返回空字符串
     */
    private String generateHorizontalLine(int length) {
        return length <= 0 ? "" : BRANCH_CONNECTOR.repeat(length);
    }

    /**
     * 格式化节点值
     * <p>
     * 如果节点需要高亮显示,会添加ANSI颜色代码。
     *
     * @param node 要格式化的节点
     * @return 格式化后的节点值字符串
     */
    private String formatNodeValue(Object node) {
        String value = treeRenderer.getNodeValue(node).toString();
        // 如果节点需要高亮,添加颜色代码
        return treeRenderer.isHighlighted(node) ? HIGHLIGHT_START + value + HIGHLIGHT_END : value;
    }
}

使用示例

  1. 可以通过重写 toString 方法,集成到你的数据结构中

    java
    // 假设我们有一个二叉树类
    class BinaryTree<T> {
        @Override
        public String toString() {
            if (size == 0) {
                return "Empty Heap";
            }
            return BinaryTreePrinter.TreePrintBuilder.<String>create()
                    .withRoot(() -> {
                        // TODO 返回根节点
                    })
                    .withChildren((node, isLeft) -> {
                        if (node == null) return null;
                        // TODO 根据 isLeft 返回左右子节点
                    })
                    .withValues(node -> {
                        if (node == null) return null;
                        // TODO 返回节点的值
                    })
                    .withHighlighting(node -> {
                        if (node == null) return false;
                        // TODO 根据节点属性决定是否高亮
                    })
                    .build();
        }
    }
  2. 直接使用 TreePrintBuilder 来打印

    java
    public static void main(String[] args) {
        // 假设我们有一个二叉树
        BinaryTree<Integer> tree = new BinaryTree<>();
    
        // 添加一些节点
        ....
    
        String result = TreePrinter.TreePrintBuilder.<String>create()
                .withRoot(() -> tree.size() == 0 ? null : 0)
                .withChildren((node, isLeft) -> {
                    if (node == null) return null;
    
                    // 根据实际节点类型调整
                    return isLeft ? node.left : node.right; 
                })
                .withValues(node -> {
                    if (node == null) return null;
                    return node.value.toString();
                })
                .withHighlighting(node -> {
                    if (node == null) return false;
    
                    // 例如红黑树中红色节点高亮
                    return node.color == Color.RED; 
                })
                .build();
        System.out.println(result);
    }

Released under the MIT License.

本站访客数 人次 本站总访问量