一个小玩意,打印最简二叉树


前言

前一段在温习数据结构时候,温习到二叉树,想把二叉树直观的展示出来好难搞,二叉树不能像数组一样,通过简单的遍历就可以直观在控制台上观察到结果,遂到网上查询有没有能把二叉树的打印出来的代码,结果感觉不是很满意,决定自己写一个打印二叉树的小工具。

解析放在后面,如果不想看过程的话,想直接拿来用。我给该文章绑定这个资源可以直接免费下载下来使用,不需要积分。

小工具的特点与效果

打印的效果

左树空白直接跳过
     648970
614655 -^---------------------------------------------------- 201291                                                                  
                                       782494 -------------------^---------------------- 561322                                       
                              990034 -----^- 384619                                  57360 -^------------------ 676398                
                   608729 -------^- 159242      ^--------- 171787 896769 --------------^                    23762 -^- 897362          
               76670 -^--- 354919                     200773 -^      ^---- 489014                 128259 -----^          ^---- 246831 
         673873 -^     42351 -^                  787867 -^            726599 -^- 629120      864720 -^- 524511            380458 -^   

泛型String,不限长度
                                                           fdsajfk
       irjgkalfdgfmd -----------------------------------------^-- 15                               
fdjs --------^------------------------------------ 14          10 -^---------------- 17            
  ^------ &*H                             594ojirt -^- 054ojre                     3 -^--- 8       
   324wre -^             05ojpr --------------^                           1 -------^   14 -^---- 9 
                      4551 -^-- 9035treijo                            18 -^-- 19            0 ---^ 
              3404wopj -^    nbmxvc -^- 845gdf                      2 -^   14 -^- 4         ^- 14 

数值较小的时候,颜值还不错
                                                                     18
                                            27 -----------------------^   
              49 ----------------------------^- 72                     
16 ------------^----------------- 79             ^----------------- 85 
 ^----- 62              91 --------^------- 4              6 --------^ 
     20 -^-- 24    40 ---^-- 44       90 ---^        86 ---^--- 3     
  42 -^   59 -^ 94 -^- 82 70 -^- 78 0 -^- 69      98 -^- 74 81 -^- 82 

70
 ^- 23                             
     ^- 50                         
         ^---------- 29            
                42 ---^- 9         
             98 -^- 42   ^- 48     
          59 -^              ^- 37 

怎么样,咱这效果可以吧,嘿嘿!😀😀

时间复杂度与空间空间复杂度

我个人估计小工具资源消耗情况:

最好空间复杂度大概是: O ( 2 N ) O(2N) O(2N),取最高次幂为 O ( N ) O(N) O(N)
最坏空间复杂度大概是: O ( N + N ∗ ( N + 1 4 − 1 ) ∗ l o g 2 N ) O(N + N * (\frac{N + 1}{4} - 1) * log_2N) O(N+N(4N+11)log2N) ,取最高次幂为 O ( N 2 ∗ l o g N ) O(N^2 * logN) O(N2logN)
空间复杂度是: O ( 2 N ) O(2N) O(2N)

最好的空间复杂度情况是没有同层级的相邻节点之间没有空白节点。
如果有空白节点的话,就需要计算空白节点的需要占据的长度,每次计算长度花费最坏空间复杂度 O ( l o g 2 N ) O(log_2N) O(log2N) ,需要计算最坏 N + 1 4 − 1 \frac{N + 1}{4} - 1 4N+11 次。

后面会展开说明这些数值是怎么来的。

小工具的优点:

支持泛型:元素的打印内容由toString方法来决定,但是要保证toString方法返回的字符串没有换行字符,否则结构可能会打印出错。

支持长度自适应:树中不同长度的元素都可以打印出完美的树形结构。

打印即是最简的状态:空白节点自动跳过不占用屏幕空间。

占用屏幕空间少:已经在尽可能保证观感的前提下,减少打印二叉树需要的屏幕空间。

小工具的缺点:

效率上可能比较差,最坏的情况达到了 O ( N 2 ∗ l o g N ) O(N^2 * logN) O(N2logN),处理大量数据的时候会慢一些。

如何打印这样的二叉树

我们现在来拆开讲解一下如何打印这样的二叉树。

子树的状态分析

在介绍代码之前,很有必要了解一下在此打印逻辑下的子树的打印状态,一颗二叉树就是由若干的子树组合而成。
子树的状态一共有9种,这9种状态有不同的“空白”字符与“破折号”字符的计算规则。

简要说明下面所写的名称代表的含义:

space : 代表所需的空白字符的长度
dashes:代表破折号字符所需的长度
space_concat:对齐打印格式所需要补偿的空格数量
PL = parent.valueLen	父元素本身的长度 - parent length
PLL = PL / 2			父元素本身的左半部分的长度 - parent left length
PRL = PL - PL / 2 - 1	父元素本身的右半部分的长度 - parent right length
注意: child 是一个相对的关系,child可以时一个节点,也可以是一个子树,
	  所以child在计算时使用的不是他自己的value的长度,使用的是打印长度。
	  比如:"- XXX" 元素本身的长度为3,他的打印长度为5
1. 裸节点
R 

裸节点是根节点的打印状态。

2. 叶节点 - 左子节点

X - 

该节点为左叶节点的打印状态,也是每个左节点默认的最短的打印状态。

3. 叶节点 - 右子节点

- X

该节点为右叶节点的打印状态,也是每个右节点默认的最短的打印状态。

4. 左父 + 左子节点

    RRR -       space = child.strlen - PLL         
XXX -^          dashes = 0
                concat_space = PRL + 2 // " -".length == 2

PS: 从这里开始要计算长度了,要根据子节点长度计算当前节点的长度,主要是为 RRR 节点,计算出合适长度的空白字符,计算出合适长度的破折号字符。

该状态需要在RRR左边补白。

5. 左父 + 右子节点

RRR ---         space = 0                     
 ^- XXX         dashes = child.strLen - PRL - 2 // child.strLen = "- XXX".length
                space_concat = PLL

该状态需要在RRR右边边补杠

6. 右父 + 左子节点

--- RRR         space = 0                      
XXX -^          dashes = child.strLen - PLL - 2
                space_concat = PRL

该状态需要在 RRR 左边补杠

7. 右父 + 右子节点

- RRR           space = child.strLen - PRL           
   ^- XXX       dashes = 0
                space_concat = PLL + 2

该状态需要在RRR右边补白

8. 左父 + 全节点

    RRR ---     space = child.strLen - PLL           
XXX -^- XXX     dashes = child.strLen - PRL - 2
                space_concat = 0

该状态需要在RRR左边补白,右边补杠。

9. 右父 + 全节点

--- RRR         space = child.strLen - PRL
XXX -^- XXX     dashes = child.strLen - PLL - 2
                space_concat = 0

该状态需要在RRR 左边补杠,右边补白。

总体打印逻辑

由于原来二叉树的包含的上下文信息太过局限,所以首先对该二叉树的节点进行包装,采用先序遍历,在包装的同时,对所有节点进行一次打印字符计算。
PS: 此时所有的节点的打印字符都计算完毕。如果该树是一个完美二叉树,那么直接中序遍历即可在控制台上打印出完美的树形结构。

接下来进行中序遍历,将节点组合打印,在中序遍历的过程中,如果同层级的两个元素不是相邻的元素,那么则会计算空白部分的长度,拼接上空白字符的长度再打印。

具体实现

必要的数据结构的准备

准备二叉树的标准结构

public class TNode<E> {

    public E value;

    public TNode<E> left;

    public TNode<E> right;

    public TNode() {}

    public TNode(E value) { this.value = value; }
    
    public void left(E value) { left = new TNode<>(value); }
    
    public void right(E value) { right = new TNode<>(value); }
    
    public String toString() {
        return value == null ? null : value.toString();
    }
}

准备二叉树节点包装类的数据结构

private static class WNode<E> {
        public TNode<E> key;
        public WNode<E> left;
        public WNode<E> right;
        public int deep;
        public int pos;
        public WNode<E> parent;
        public String str;

        public WNode(TNode<E> key) { this.key = key; }
        
        public boolean isHeader() { return parent == null; }

        public boolean hasLeft() { return this.left != null; }

        public boolean hasRight() { return this.right != null; }

        public boolean hasChild() { return hasLeft() || hasRight(); }

        public boolean hasNoChild() { return left == null && right == null; }

        public boolean isLeft() { return parent.left == this; }

        public boolean isRight() { return parent.right == this; }

        public boolean hasBro() { return parent.left != null && parent.right != null; }

        public E value() { return this.key.value; }

        public String valueStr() { return key.value == null ? "#" : key.value.toString(); }

        public int valueLen() { return valueStr().length(); }

        public int valueLeftLen() { return valueLen() / 2; }

        public int valueRightLen() { return valueLen() - valueLen() / 2 - 1; }

        public String toString() { return (isHeader() ? "H" : isLeft() ? "L" : "R") + ": " + valueStr(); }

    }

实现过程

首先先序遍历,包装一边原来的节点

public void wrapNode(TNode<E> root) {
        if (root == null) return;
        this.tree = new WNode<>(root);
        this.tree.deep = 1;
        this.tree.pos = 0;
        this.deep = 1;
        wrapNode(root,this.tree,this.deep);
    }

public void wrapNode(TNode<E> tRoot,WNode<E> wRoot,int deep) {
    if (this.deep < deep) this.deep = deep;
    if (tRoot.left != null) {
        wRoot.left = new WNode<E>(tRoot.left);
        wRoot.left.deep = deep + 1;
        wRoot.left.pos = wRoot.pos * 2;
        wRoot.left.parent = wRoot;
        wrapNode(tRoot.left,wRoot.left,deep + 1);
    }
    if (tRoot.right != null) {
        wRoot.right = new WNode<E>(tRoot.right);
        wRoot.right.deep = deep + 1;
        wRoot.right.pos = wRoot.pos * 2 + 1;
        wRoot.right.parent = wRoot;
        wrapNode(tRoot.right,wRoot.right,deep + 1);
    }
    wRoot.str = calNodeStr(wRoot);
}

涉及到的拼接算法

public String calNodeStr(WNode<E> node) {
    String str = node.valueStr();
    int LL = str.length() / 2; // left len
    int RL = str.length() - LL - 1; // right len; RL = str.length() - LL - "^".length()
    int LSL = node.left == null ? 0 : node.left.str.length();
    int RSL = node.right == null ? 0 : node.right.str.length();

    if (node.parent == null) // 根节点
        return node.hasLeft() ? repeatSpace(LSL - LL) + str : str;

    str = node.isLeft() ? str + " -" : "- " + str;

    if (!node.hasChild()) // 没有孩子
        return str;

    if (node.hasLeft() && node.isLeft()) // 在左边补充空格
        str = repeatSpace(LSL - LL) + str;
    else if (node.hasRight() && node.isRight()) // 在右边补充空格
        str = str + repeatSpace(RSL - RL);

    if (node.hasLeft() && node.isRight()) // 在左边补充破折号
        str = repeatDashes(LSL - LL - 2) + str;
    else if (node.hasRight() && node.isLeft()) // 在右边补充破折号
        str = str + repeatDashes(RSL - RL - 2);

    return str;
}

之后中序遍历,从上到下逐层打印

在这里将组合左右子树,组织树形结构

public void printTree() {
    if (this.tree == null) return;
    System.out.println(this.tree.str);
    if (this.tree.hasNoChild()) return;
    WNode<E> pre = null;
    WNode<E> cur = null;
    int deep = 1;
    LinkedList<WNode<E>> queue = new LinkedList<>();
    queue.add(tree);
    while (!queue.isEmpty()) {
        pre = cur;
        cur = queue.poll();
        if (deep < cur.deep) {
            System.out.println();
            deep = cur.deep;
            pre = null;
        }
        String str = concatChildNode(cur, pre);
        System.out.print(str);

        if (cur.hasLeft() && cur.left.hasChild())
            queue.add(cur.left);
        if (cur.hasRight() && cur.right.hasChild())
            queue.add(cur.right);
    }
    System.out.println();
}
public String concatChildNode(WNode<E> node,WNode<E> pre) {
 	String str = concatChildNode(node);
    int spaceCount = calSpaceBetweenNode(node, pre);
    return repeatSpace(spaceCount) + str + ' ';
}

拼接时需要注意空白节点

public String concatChildNode(WNode<E> node) {
    if (node.hasNoChild()) throw new RuntimeException("进入此方法的节点 必须为 非叶节点 !");

    if (node.hasLeft() && node.hasRight())
        return node.left.str + "^" + node.right.str;

    String str = node.valueStr();
    int LL = str.length() / 2; // left len
    int RL = str.length() - LL - 1; // right len; RL = str.length() - LL - "^".length()

    if (node.isHeader() && node.hasLeft())  // Header的处理方式和节点为左根的处理方式一样
        return node.left.str + "^" + repeatSpace(RL + 2);
    if (node.isHeader() && node.hasRight())
        return repeatSpace(LL) + "^" + node.right.str;

    if (node.isLeft() && node.hasLeft())
        return node.left.str + "^" + repeatSpace(RL + 2);
    if (node.isLeft() && node.hasRight())
        return repeatSpace(LL) + "^" + node.right.str;

    if (node.isRight() && node.hasLeft())
        return node.left.str + "^" + repeatSpace(RL);
    if (node.isRight() && node.hasRight())
        return repeatSpace(LL + 2) + "^" + node.right.str;

    throw new RuntimeException("未知错误发生在: " + node);
}
public int calSpaceBetweenNode(WNode<E> cur,WNode<E> pre) {
    //if (root.deep != pre.deep) throw new RuntimeException();
    if (cur.parent == null) // 根节点没有间隔
        return 0;
    // 当cur为当前层的第一个节点时
    int prePos = pre == null ? 0 : pre.pos + 1;
    int len = cur.pos - prePos;
    if (len <= 0) return 0;
    int count = 0;
    for (int pos = prePos; pos < cur.pos;) {
        // TODO 这里应该是要+1
        count += calNodeLength(tree, cur.deep, pos) + 1;
        pos = calJumpPos(tree, cur.deep, pos);
    }
    return count;
}

public int calJumpPos(WNode<E> root,int deep,int pos) {
    WNode<E> node = findNodeUntilNull(root, deep, pos);
    if (node.deep == deep && node.pos == pos)
        return node.pos + 1;
    if (node.deep == deep)
        throw new RuntimeException("未在" + deep + "层数找到" + pos + "号位置的节点");
    int direct = (int) (pos / Math.pow(2,deep - node.deep - 1));
    int curPos = node.pos * 2 + (direct % 2 == 0 ? 0 : 1);
    return (curPos + 1) * (int)Math.pow(2,deep - (node.deep + 1));
}

如何使用

new TreePrinter<>(tree).printTree();
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值