形象直观地打印二叉树

/**
 * 二叉树节点
 *
 * @author HetFrame
 * @date 2022/4/1 21:26
 */

public class TreeNode<T> {
    /**
     * 值
     */
    T value;

    /**
     * 左孩子
     */
    TreeNode<T> left;
    /**
     * 右孩子
     */
    TreeNode<T> right;

    public TreeNode() {
    }

    public TreeNode(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }

    public TreeNode<T> getLeft() {
        return left;
    }

    public void setLeft(TreeNode<T> left) {
        this.left = left;
    }

    public TreeNode<T> getRight() {
        return right;
    }

    public void setRight(TreeNode<T> right) {
        this.right = right;
    }

    @Override
    public String toString() {
        return "TreeNode{" +
                "value=" + value +
                ", leftChild=" + left +
                ", rightChild=" + right +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof TreeNode)) {
            return false;
        }
        TreeNode<?> treeNode = (TreeNode<?>) o;
        return getValue().equals(treeNode.getValue()) && Objects.equals(getLeft(), treeNode.getLeft()) && Objects.equals(getRight(), treeNode.getRight());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getValue(), getLeft(), getRight());
    }
}
    /**
     * 形象直观地打印二叉树
     * <pre>
     *     1.层序遍历得到所有节点
     *     2.每个节点转换成PrintNode,并确定宽度,x轴方向的偏移量
     *       (1)x需要从最底层开始计算,最底层两个叶子节点按宽度相邻,即一个节点打印完宽度后,立即打印第二个节点
     *       父节点位于两个叶子节点间的中线上方。
     *
     *     3.打印
     *             +
     *     ┌———————┴———————┐
     *     +               *
     * ┌———┴———┐       ┌———┴———┐
     * a       *       +       g
     *       ┌—┴—┐   ┌—┴—┐
     *       b   c   *   f
     *              ┌┴┐
     *              d e
     *
     * </pre>
     *
     * @param tree 树
     */
    public static <T> void printBinaryTreeVisually(TreeNode<T> tree) {
        if (tree == null) {
            return;
        }

        //树的深度
        int depth = findDepth(tree);

        //转换树
        PrintNode<T> printTree = convertToPrintNode(tree, null, depth, 0, null);

        //节点宽度 x
        printTree.width = findWidth(printTree, 0);

        //层节点的list
        List<List<PrintNode<T>>> nodesList = new ArrayList<>();

        //遍历完一层后,应该将这层每个节点的子节点都放入队列中,等待下一次遍
        Queue<PrintNode<T>> queue = new LinkedList<>();
        queue.add(printTree);

        while (!queue.isEmpty()) {
            List<PrintNode<T>> curLevel = new ArrayList<>();
            //将该层的节点放入list
            while (!queue.isEmpty()) {
                PrintNode<T> cur = queue.poll();
                if (cur.parent != null) {
                    PrintNode<T> parent = cur.parent;
                    cur.width = parent.width;
                }
                curLevel.add(cur);
            }
            nodesList.add(curLevel);

            //将该层子节点放入队列中
            for (PrintNode<T> node : curLevel) {
                if (node.left != null) {
                    queue.add(node.left);
                }
                if (node.right != null) {
                    queue.add(node.right);
                }
            }

        }


        //找到最后一层中的实节点,作为起始定位点。确定最后一层节点偏移量。
        List<PrintNode<T>> lastLevel = nodesList.get(depth - 1);
        for (int i = 0; i < lastLevel.size(); i++) {
            PrintNode<T> first = lastLevel.get(i);
            if (!first.isVirtual) {
                first.offsetX = 0;
                //first左边的节点
                for (int j = i - 1; j >= 0; j--) {
                    //左边的节点偏移量减少一个单位的宽度
                    lastLevel.get(j).offsetX = lastLevel.get(j + 1).offsetX - first.width;
                }
                //first右边的节点
                for (int k = i + 1; k < lastLevel.size(); k++) {
                    //右边的节点偏移量增加一个单位的宽度
                    lastLevel.get(k).offsetX = lastLevel.get(k - 1).offsetX + first.width;
                }
                break;
            }
        }


        //从倒数第二层开始,根据下一层偏移量计算出该层的偏移量
        for (int i = depth - 2; i >= 0; i--) {
            List<PrintNode<T>> level = nodesList.get(i);
            //遍历每层节点
            for (PrintNode<T> node : level) {
                //位于左右孩子的中线上
                node.offsetX = (node.left.offsetX + node.right.offsetX) / 2;
            }
        }

        //查找应该补充的偏移量,然后将所有实节点的偏移量变为正数
        int offsetSup = findOffsetLeft(printTree, 0);
        //将树向右平移对应的偏移量
        fixOffset(printTree, offsetSup);

        //打印树
        for (List<PrintNode<T>> printNodes : nodesList) {
            //记录当前打印的x轴位置
            int x = 0;
            for (PrintNode<T> printNode : printNodes) {
                if (printNode.isVirtual) {
                    continue;
                }

                StringBuilder fillers = new StringBuilder();
                for (; x < printNode.offsetX; x++) {
                    fillers.append(" ");
                }
                System.out.print(fillers);
                System.out.print(printNode.value);
                //打印完一个节点后,x加上字符的长度
                x += printNode.selfWidth;
            }


            //如果有子节点,打印出线条
            System.out.println();
            int y = 0;
            String filler = " ";
            for (PrintNode<T> printNode : printNodes) {
                if (printNode.isVirtual || (printNode.left == null && printNode.right == null) || (printNode.left.isVirtual && printNode.right.isVirtual)) {
                    continue;
                }

                StringBuilder fillers = new StringBuilder();
                for (; y < printNode.offsetX; y++) {
                    //有左孩子,从它开始打印树枝
                    if (printNode.left != null && !printNode.left.isVirtual && printNode.left.offsetX == y) {
                        fillers.append("┌");
                        filler = "—";
                        continue;
                    }

                    fillers.append(filler);
                }

                if (printNode.left != null && !printNode.left.isVirtual && printNode.right != null && !printNode.right.isVirtual) {
                    fillers.append("┴");
                } else if (printNode.left != null && !printNode.left.isVirtual) {
                    fillers.append("┘");
                } else if (printNode.right != null && !printNode.right.isVirtual) {
                    fillers.append("└");
                }

                //有右孩子则延续到右孩子
                if (printNode.right != null && !printNode.right.isVirtual) {
                    for (; y < printNode.right.offsetX; y++) {
                        filler = "—";
                        //有左孩子,从它开始打印树枝
                        if (printNode.right.offsetX - 1 == y) {
                            fillers.append("┐");
                            filler = " ";
                            continue;
                        }
                        fillers.append(filler);
                    }
                }

                System.out.print(fillers);
                filler = " ";
                //打印完一个节点后,x加上字符的长度
                y++;
            }
            System.out.println();
        }

    }
    /**
     * 将{@link TreeNode}转换为{@link PrintNode}
     * <p>当不存在某子节点时,生成虚拟节点,存坐标</p>
     *
     * @param tree     {@link TreeNode}
     * @param parent   打印节点的父节点
     * @param depth    树的深度,便于生成虚拟节点
     * @param curDepth 当前深度
     * @param isLeft   是否为父节点的左孩子
     * @return {@link PrintNode}
     */
    private static <T> PrintNode<T> convertToPrintNode(TreeNode<T> tree, PrintNode<T> parent, int depth, int curDepth, Boolean isLeft) {
        PrintNode<T> cur = new PrintNode<>();
        ++curDepth;
        if (tree == null) {
            //超过深度返回null
            if (curDepth > depth) {
                return null;
            }
            //没超过深度则创建一个虚拟节点
            cur.isVirtual = true;
            tree = new PrintNode<>();
        }


        cur.value = tree.value;
        //节点值的自身宽度
        if (cur.value != null) {
            cur.selfWidth = cur.value.toString().length();
        }
        cur.parent = parent;
        cur.isLeft = isLeft;

        cur.left = convertToPrintNode(tree.left, cur, depth, curDepth, true);
        cur.right = convertToPrintNode(tree.right, cur, depth, curDepth, false);

        return cur;
    }

    /**
     * 查找树节点值的最大宽度。
     *
     * @param tree 树
     * @param max  当前最大值
     * @return int 最大值
     */
    private static <T> int findWidth(TreeNode<T> tree, int max) {
        if (tree == null) {
            return 0;
        }

        //为保证美观性,宽度应大于字符串长度,所以直接+1
        max = Math.max(tree.value.toString().length() + 1, max);
        //为奇数时+1,宽度只能为偶数,否则父节点计算出的偏移量会出现0.5
        if (max % 2 == 1) {
            max++;
        }
        int left = 0;
        int right = 0;

        if (tree.left != null) {
            left = findWidth(tree.left, max);
        }
        if (tree.right != null) {
            right = findWidth(tree.right, max);
        }

        return Math.max(Math.max(left, right), max);
    }

    /**
     * 计算树的深度
     *
     * @param tree 树
     * @return int 深度
     */
    public static <T> int findDepth(TreeNode<T> tree) {
        return findDepth(tree, 0);
    }

    /**
     * 递归查找树的深度
     *
     * @param tree  树
     * @param depth 当前深度
     * @return int 深度
     */
    private static <T> int findDepth(TreeNode<T> tree, int depth) {
        if (tree == null) {
            return 0;
        }

        depth++;

        int leftDepth = 0;
        int rightDepth = 0;

        if (tree.left != null) {
            leftDepth = findDepth(tree.left, depth);
        }

        if (tree.right != null) {
            rightDepth = findDepth(tree.right, depth);
        }

        return Math.max(Math.max(leftDepth, rightDepth), depth);
    }

    private static <T> int findOffsetLeft(PrintNode<T> tree, int offset) {
        if (tree == null || tree.isVirtual) {
            return 0;
        }

        offset = Math.max(-tree.offsetX, offset);

        int offsetL = 0;
        int offsetR = 0;

        if (tree.left != null) {
            offsetL = findOffsetLeft(tree.left, offset);
        }
        if (tree.right != null) {
            offsetR = findOffsetLeft(tree.right, offset);
        }

        return Math.max(Math.max(offsetL, offsetR), offset);
    }

    /**
     * 修正偏移量
     *
     * @param tree   树
     * @param offset 偏移量
     */
    private static <T> void fixOffset(PrintNode<T> tree, int offset) {
        if (tree == null) {
            return;
        }

        tree.offsetX += offset;

        if (tree.left != null) {
            fixOffset(tree.left, offset);
        }
        if (tree.right != null) {
            fixOffset(tree.right, offset);
        }
    }

    /**
     * 待打印的节点
     *
     * @author HetFrame
     * @date 2023/2/2 21:55
     */
    public static class PrintNode<T> extends TreeNode<T> {
        /**
         * 左孩子
         */
        PrintNode<T> left;
        /**
         * 右孩子
         */
        PrintNode<T> right;
        /**
         * 节点自身宽度(一棵树中每个节点的宽度一样)
         */
        int width;
        /**
         * 节点值的打印宽度
         */
        int selfWidth;
        /**
         * x轴向右偏移量
         */
        int offsetX;
        /**
         * parent
         */
        PrintNode<T> parent;
        /**
         * 是否虚拟,虚拟节点用于计算位置
         */
        boolean isVirtual;
        /**
         * 是父节点的左孩子
         */
        Boolean isLeft;
    }
测试
		TreeNode<String> tree = new TreeNode<>();

        Random random = new Random();
        List<String> strings = new ArrayList<>();
        strings.add("M");
        strings.add("Java");
        strings.add("Tomcat");
        strings.add("Linux");
        strings.add("Elastic");
        strings.add("rm/*-rf");
        strings.add("Tom");
        strings.add("Apollo");
        strings.add("0_0");
        strings.add("Eureka");

        Queue<TreeNode<String>> stack = new LinkedList<>();
        stack.add(tree);
        tree.value = "HetFrame";
        TreeNode<String> cur;
        Queue<TreeNode<String>> nodes = new LinkedList<>();
        for (int i = 0; i < 20; i++) {
            TreeNode<String> newNode = new TreeNode<>(strings.get(random.nextInt(10)));
            nodes.add(newNode);
        }

        while (!stack.isEmpty()) {
            cur = stack.poll();
            int ran = random.nextInt(10);
            if (nodes.isEmpty()) {
                continue;
            }
            cur.left = nodes.poll();
            cur.right = nodes.poll();
            if (ran < 3) {
                cur.left = null;
            }
            if (ran > 8) {
                cur.right = null;
            }
            if (cur.left != null) {
                stack.add(cur.left);
            }
            if (cur.right != null) {
                stack.add(cur.right);
            }
        }
        printBinaryTreeVisually(tree);
结果
                                                            HetFrame
                    ┌———————————————————————————————————————┴———————————————————————————————————————┐
                    Java                                                                            Tom
┌———————————————————┴———————————————————┐                                       ┌———————————————————┴———————————————————┐
Java                                    Eureka                                  Linux                                   0_0
└—————————┐                             └—————————┐                             └—————————┐                             └—————————┐
          Apollo                                  rm/*-rf                                 Tom                                     Elastic
     ┌————┘                                  ┌————┴————┐                             ┌————┴————┐
     Eureka                                  Java      Tom                           Linux     Apollo
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值