文章目录
前言
前一段在温习数据结构时候,温习到二叉树,想把二叉树直观的展示出来好难搞,二叉树不能像数组一样,通过简单的遍历就可以直观在控制台上观察到结果,遂到网上查询有没有能把二叉树的打印出来的代码,结果感觉不是很满意,决定自己写一个打印二叉树的小工具。
解析放在后面,如果不想看过程的话,想直接拿来用。我给该文章绑定这个资源可以直接免费下载下来使用,不需要积分。
小工具的特点与效果
打印的效果
左树空白直接跳过
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+1−1)∗log2N) ,取最高次幂为
O
(
N
2
∗
l
o
g
N
)
O(N^2 * logN)
O(N2∗logN)
空间复杂度是:
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+1−1 次。
后面会展开说明这些数值是怎么来的。
小工具的优点:
支持泛型:元素的打印内容由toString方法来决定,但是要保证toString方法返回的字符串没有换行字符,否则结构可能会打印出错。
支持长度自适应:树中不同长度的元素都可以打印出完美的树形结构。
打印即是最简的状态:空白节点自动跳过不占用屏幕空间。
占用屏幕空间少:已经在尽可能保证观感的前提下,减少打印二叉树需要的屏幕空间。
小工具的缺点:
效率上可能比较差,最坏的情况达到了 O ( N 2 ∗ l o g N ) O(N^2 * logN) O(N2∗logN),处理大量数据的时候会慢一些。
如何打印这样的二叉树
我们现在来拆开讲解一下如何打印这样的二叉树。
子树的状态分析
在介绍代码之前,很有必要了解一下在此打印逻辑下的子树的打印状态,一颗二叉树就是由若干的子树组合而成。
子树的状态一共有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();