满二叉树的逻辑结构表示

前言:
最近复习了堆排序的内容, 之前学习过的东西忘了一个精光, 感觉但是也多是死记硬背, 没有很好的去探索为什么这样做. 所以, 在思考数组存储的完全二叉树的时候, 想着可以把树打印出来看看. 这个不是什么难事, 只需要一个简单的程序遍历就行了. 不过, 我觉得这样单纯逐层打印数字, 不是很美观. 所以想着来给它做一个优化版本的打印数组存储的满二叉树!

我们先来看效果图吧!
在这里插入图片描述
这是一个 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} a1q1qn
这里 a = 1, q = 2, 所以一颗 n 层的满二叉树的节点数是: 2 n − 1 2^n-1 2n1

在我们使用数组表示二叉树时, 第一个位置一般不存放实际的元素, 把它当作哨兵元素, 同时也能降低复杂度. 所以实际上数组的元素个数为: 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 图, 平时可是见不到这玩意的!
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值