前言:
最近复习了堆排序的内容, 之前学习过的东西忘了一个精光, 感觉但是也多是死记硬背, 没有很好的去探索为什么这样做. 所以, 在思考数组存储的完全二叉树的时候, 想着可以把树打印出来看看. 这个不是什么难事, 只需要一个简单的程序遍历就行了. 不过, 我觉得这样单纯逐层打印数字, 不是很美观. 所以想着来给它做一个优化版本的打印数组存储的满二叉树!
我们先来看效果图吧!
这是一个 5 层的满二叉树, 我可以调整参数, 使之打印更多层的树, 不过 5 层已经是我的控制台的极限了. 因为再多的话, 排版就会错乱了. 不过可以将输出结构写入文件, 这样就不受控制台的限制了. 我尝试过一个 10 层, 1023 个节点的满二叉树, 效果和这个相同.
等比数列复习
这里针对是满二叉树, 所以我们需要复习一下等比数列的知识.
对于一颗满二叉树, 我们可以知道:
第 1 层节点数是: 1
第 2 层节点数是: 2
第 3 层节点数是: 4
…
第 n 层节点数是: 2^n
这里一眼可以看出, 它就是高中学习过的等比数列了. 所以, 一颗 n 层的满二叉树的节点数就是求等比数列: 1, 2, 4, …, 2^n 的和. 所以, 这就很简单了, 等比数列的求和公式为:
a
1
−
q
n
1
−
q
a\frac{1-q^n}{1-q}
a1−q1−qn
这里 a = 1, q = 2, 所以一颗 n 层的满二叉树的节点数是:
2
n
−
1
2^n-1
2n−1
在我们使用数组表示二叉树时, 第一个位置一般不存放实际的元素, 把它当作哨兵元素, 同时也能降低复杂度. 所以实际上数组的元素个数为:
2
n
2^n
2n, 所以后面我会这样来表示数组的长度: 1<<floor
, 这是一个简单的左移操作符, 它就是计算
2
n
2^n
2n 的快捷方式.
代码
生成树的字符串表示是使用一个递归函数生成的, 它的主要过程是进行字符串的拼接, 从底部往上层进行递归生成. 这里来简单介绍一下它的组成, 直接看图吧.
整个树最下层的数字的位置是根据数字本身的位数来计算的, 每个数字之间的间隔是固定的 (这里我设置的是 3 个空格), 然后数字的中间位置决定了它上一层竖线 | 的位置, 两个竖线的间隔, 决定了下划线 __ 的长度, 下划线的中间最后用 | 来代替. 这样一层计算出来了, 然后递归整个过程就行了.
所以, 这里最底下的一层, 我称之为基底, 及它上面的竖线的位置, 是单独生成的, 后来的结构都是递归生成的了. 剩下的直接看代码吧!
// 完全二叉树的一些操作, 学习堆的前置知识.
package main
import (
"fmt"
"log"
"strings"
)
const (
// 最底层的数字元素之间的最小宽度为 3 个空格
MinWidth = " "
)
func main() {
floor := 10 // 树的层数
tree := make([]int, 1<<floor)
// 初始化树, 下标即元素, 注意 0 号位置是哨兵, 或者说没有意义.
for i := 0; i < len(tree); i++ {
tree[i] = i
}
img := DisplayTree(tree)
fmt.Println(img)
}
// Samples:
// 8
// ________|________
// | |
// 8 8
// ____|____ ____|____
// | | | |
// 8 8 8 8
// __|__ __|__ __|__ __|__
// | | | | | | | |
// 8 8 8 8 8 8 8 8
// 345
// _______________|________________
// | |
// 785 34
// _____|______ _________|________
// | | | |
// 4354 23 113243 99
// ___|___ ___|____ ____|_____ ____|____
// | | | | | | | |
// 142 378 1 274817 24871 284781 27482 71481
func DisplayTree(tree []int) string {
// 获取树的层数
floor := getTreeFloor(len(tree))
// 先获取基底的位置
bottom := tree[len(tree)>>1:]
// 存储计算得到的上一层结构的位置数据
positions := computeBase(bottom, len(MinWidth))
// 递归计算树的结构
return computePosition(tree, positions, floor)
}
func computePosition(tree, positions []int, floor int) string {
// 层数为 0, 退出递归
if floor == 0 {
return ""
}
// 获取当前层的所有元素
es := tree[1<<(floor-1) : 1<<floor]
// 开始递归前, 将其减 1
floor--
// 存储树的字符串结构
var builder strings.Builder
// 上一层的位置
nextPositions := make([]int, 0)
// 使用数组来存储上层的元素, 因为数字会比竖线和下划线长, 所以这里多预留一些位置
elements := make([]string, positions[len(positions)-1]+25)
// 使用数组来存储上层的元素, 竖线 |
verticals := make([]string, positions[len(positions)-1]+1)
// 使用数组来存储上层结构, 下划线 __|__
underlines := make([]string, positions[len(positions)-1]+1)
// 初始化上层结构 竖线和下划线 的字符串数组
for i := 1; i < len(verticals); i++ {
elements[i] = " "
verticals[i] = " "
underlines[i] = " "
}
// 在对应位置上赋值 |
k := 0
for _, pos := range positions {
// 这个判断是为了去掉最后一个头上的竖线 |, 你可以试一试
if len(positions) > 1 {
verticals[pos] = "|"
}
t := getDigit(es[k])
if t%2 != 0 {
t = t / 2
} else {
t = t/2 - 1
}
s := fmt.Sprintf("%d", es[k])
start := pos - t
for i := 0; i < len(s); i++ {
// 这里是操作是为了把需要的元素放到它所在位置的中间.
// 例如: 11445, 它的位数是 5, 所以左右放两个,
// 右边放两个, 中间是对应竖线的位置, 视觉上比较舒服
elements[start] = string(s[i])
start++
}
k++
}
// 在对应位置上赋值 __|__
for i := 0; i < len(positions)-1; i += 2 {
// 首先全部变成下划线 _, 然后把中间的变成 |, 这样处理起来比较简单.
for j := positions[i]; j <= positions[i+1]; j++ {
underlines[j] = "_"
}
pos := (positions[i] + positions[i+1]) / 2
nextPositions = append(nextPositions, pos) // 这个竖线的位置即是上一层的位置
underlines[pos] = "|"
}
builder.WriteString(strings.Join(underlines, ""))
builder.WriteString("\n")
builder.WriteString(strings.Join(verticals, ""))
builder.WriteString("\n")
builder.WriteString(strings.Join(elements, ""))
builder.WriteString("\n")
return computePosition(tree, nextPositions, floor) + builder.String()
}
func computeBase(arr []int, width int) []int {
// 根据最后一层叶子节点来计算整个树的基底
// 存储计算得到的上一层结构的位置数据
positions := make([]int, 0)
// 计算第一个点的初始位置, 它比较特殊, 单独处理
t := getDigit(arr[0])
if t%2 != 0 {
positions = append(positions, t/2+1)
} else {
positions = append(positions, t/2)
}
// 计算剩下点的位置
pos := 0
for i := 1; i < len(arr); i++ {
pos += getDigit(arr[i-1]) + width
t := getDigit(arr[i])
if t%2 != 0 {
positions = append(positions, pos+getDigit(arr[i])/2+1)
} else {
positions = append(positions, pos+getDigit(arr[i])/2)
}
}
return positions
}
func getTreeFloor(n int) int {
// n tree's len
// compute tree's heigh through it's size
// 通过树的 size 计算树的高度
if n < 0 {
log.Fatal(fmt.Errorf("Invalid Input."))
}
if n == 0 {
return 0
}
floor := 0
for {
n = n >> 1
if n == 0 {
break
}
floor++
}
return floor
}
func getDigit(e int) int {
// 输入一个元素, 计算数字的位数
// input a number, compute digits of number.
n := 1
for {
e = e / 10
if e == 0 {
break
}
n++
}
return n
}
说明:
这个代码倒不是说非要看懂, 因为本身基于字符串拼接的, 理解起来就挺麻烦的. 而且代码也是经过我多次修改, 也不是一次就能写成的. 所以, 它最好的方式辅助你理解和学习树的数据结构, 你把它移植到自己的擅长的语言中, 这样当你想要打印一个树的时候可以使用它. 不过我想要提示的是, 由于这里整个树的结构是由最底下的一层决定的, 所以如果你的中间的层长度超过了下一层, 即元素的位数太大了, 程序会无法工作的.
PS: 这个东西只是个人一时兴起的玩具, 完全是自己摸索着玩的. 如果真的需要图形化展示树的话, 我推荐使用 echarts, dot, mermaid 等工具, 它们生成的图像是有很好的排版的 (可以参考我之前的博客, 里面有详细的介绍). 不过, 我这个也不是一无是处, 至少它可以在命令行显示, 哈哈.
最后, 附赠一颗 10 层的满二叉树的 GIF 图, 平时可是见不到这玩意的!