前言
在LeetCode的刷题过程中,遇到二叉树的题目的时候,你一定会想,这二叉树的题目我在本地验证也太麻烦了吧。二叉树的初始化需要一个节点一个节点的赋值,而改变二叉树之后的二叉树的样子又不能直观的展现。你一定十分希望能够实现简单的生成一个二叉树,并且能够方便的看到自己的二叉树的具体内容。上述两点其实已经在文章《使用LeetCode方式初始化二叉树》,以及文章《重写LeetCode中的二叉树的toString方法》有了具体实现,那今天这篇文章,希望能让你能够更直观的看到二叉树具体是什么样子。如果想直接看代码的话可以直接拉到文章最后面复制代码哦
由易到难 - 先打印节点值为1~9的满二叉树
打印一个二叉树,想想就很复杂嘛!不过不用着急,复杂的代码也是一步步从简单代码改进而来的,我们先来试着打印一颗所有节点值为1,且层数为3的满二叉树。
代码
class TreeNode(var `val`: Int) {
var left: TreeNode? = null
var right: TreeNode? = null
constructor(v0: Int, vararg v: Int?) : this(v0) {
// 省略,见文章《使用LeetCode方式初始化二叉树》
}
private fun toArray(): List<Int?> {
// 省略,见文章《重写LeetCode中的二叉树的toString方法》
}
override fun toString() = toArray().toString()
// 打印二叉树
fun printNode() {
val queue = LinkedList<TreeNode?>()
queue.offer(this)
while (queue.isNotEmpty()) {
val size = queue.size
for (i in 0 until size) {
val temp = queue.pop()
print("${temp?.`val` ?: "N"} ")
if (temp != null) {
queue.offer(temp.left)
queue.offer(temp.right)
}
}
println()
}
}
}
fun main() {
TreeNode(1, 1, 1, 1, 1, 1, 1).printNode()
}
// 代码结果:
1
1 1
1 1 1 1
N N N N N N N N
代码分析
可以看到,我们依然使用了层序遍历来打印二叉树,毕竟二叉树是自上而下的,并将二叉树的空节点打印成Nprint("${temp?.`val` ?: "N"} ")
,这里使用了kotlin特有的空判断处理符号?.
和?:
,有不清楚这两个操作符的童鞋可以看这里,但是确实有一些问题,遍历结果中的节点并不像一个树,而是简单的排列起来而已,不过这个也很好解决,我们只需要在代码中加入层信息,然后按照层数打印出对应的空格数即可,而层数信息通过一个简单的二叉树遍历得到。
获取二叉树高度
class TreeNode(var `val`: Int) {
var left: TreeNode? = null
var right: TreeNode? = null
// 省略无关代码......
// 拿到二叉树的高度
fun getHigh(): Int = 1 + (this.left?.getHigh() ?: 0).coerceAtLeast(this.right?.getHigh() ?: 0)
}
加入层级信息
现在我们可以把printNode()
方法稍微改进一下
class TreeNode(var `val`: Int) {
var left: TreeNode? = null
var right: TreeNode? = null
constructor(v0: Int, vararg v: Int?) : this(v0) {
// ...
}
// 层序遍历,并保存在一个数组中,并删除尾部多余null
private fun toArray(): List<Int?> {
// ...
}
override fun toString() = toArray().toString()
fun getHigh(): Int = 1 + (this.left?.getHigh() ?: 0).coerceAtLeast(this.right?.getHigh() ?: 0)
fun printNode() {
val queue = LinkedList<TreeNode?>()
val high = getHigh()
var level = 0
queue.offer(this)
while (queue.isNotEmpty()) {
level++
val size = queue.size
for (i in 0 until size) {
val temp = queue.pop()
val blank = " " * ((1 shl (high - level + 1)) - 1)
if (i == 0) {
print("$blank${temp?.`val` ?: "N"}")
} else {
print("${blank * 2} ${temp?.`val` ?: "N"}")
}
if (temp != null) {
queue.offer(temp.left)
queue.offer(temp.right)
}
}
println()
}
}
operator fun String.times(n: Int) = (1..n).joinToString("") { this }
}
代码分析
为了方便的打印出多个空格,在类中加入一个String的扩展方法
operator fun String.times(n: Int) = (1..n).joinToString("") { this }
如果你想打印50个空格,则只需要这么用:println(" " * 50)
,至于为啥能这么写,大家可以点这里作为参考,下面分析一下核心代码。
其实相比前面的逻辑只需要在合适的地方加入正确数量的空格,下图是我们希望打印出的二叉树的样子
可以发现,每层最前面的空格数分别为
0,1,3,7
,而每层两个节点之间的空格数为1,3,7,15
,这不就是2^n - 1
吗,而当前的层数n
可以用表达式high - level + 1
来表示,所以很容易将空格的打印分为两种情况:
- 首次打印每层的空格:空格数为
2^n - 1
,在代码中可写为(shl
相当于左移,等价于2^n
):
val blank = " " * ((1 shl (high - level + 1)) - 1)
- 否则,打印的是两个节点之间的空格,正好为首次空格的两倍再减一,于是空格数可以这么写:
"${blank * 2} "
这里使用了字符串模板,以及我们刚才定义的方法String.times()
,既然定义了这么个方法,就得灵活使用不是吗^_^!
验证目前的代码
fun main() {
TreeNode(1, 1, 1, 1, 1, 1, 1).printNode()
}
// 打印结果:
1
1 1
1 1 1 1
N N N N N N N N
到目前为止,我们已经初步实现了二叉树的打印,这个二叉树的打印已经能基本满足可视化二叉树的需求,但是还有一些情况我们没有考虑到,我们仅能打印满二叉树,对于各处存在空的二叉树,还存在一些问题:
- 二叉树的值目前也仅能支持1个字符的值,如果值多了,也会让二叉树变得很难看
- 无法正确显示空节点
fun main() {
TreeNode(5, 1, 4, null, null, 3, 6).printNode()
TreeNode(1, 2, 2, -30, 40, 540, 30).printNode()
}
// 打印结果
5
1 4
N N 3 6
N N N N
1
2 2
-30 40 540 30
N N N N N N N N
下面让我们先处理字符串宽度的适配问题。
让打印出来的二叉树能适配节点字符的宽度
问题来了,二叉树的根节点值有大有小,有正有负,导致二叉树节点值的宽度有宽有窄,怎么才能完美适配而不影响二叉树的整体结构呢?当然非常简单,我们只需要遍历整个二叉树,得到所有节点中最宽的节点值,并以它的宽度为基准来画二叉树就可以了。为了方便拿到节点值的字符宽度,定义以下扩展方法:
private fun Int.charWidth(radix: Int = 10): Int = this.toString(radix).length
fun main() {
val a = "12345"
println(a.charWidth())
}
// 输出
5
这样就可以直接像Int
的成员方法一样调用charWidth
方法了,入参给了一个默认值为10
,表示整形转成字符串的进制。
下面是获取二叉树节点最大宽度的代码:
class TreeNode(var `val`: Int) {
var left: TreeNode? = null
var right: TreeNode? = null
// 省略部分代码
// ......
private fun Int.charWidth(radix: Int = 10): Int = this.toString(radix).length
fun getValWidth(): Int =
maxOf(this.`val`.charWidth(), this.left?.getValWidth() ?: 0, this.right?.getValWidth() ?: 0)
}
代码说明
想要拿到整个二叉树的最宽节点,考虑当前节点值与左右子节点的最大宽度值即可,将三个值取最大值,得到二叉树的最大字符宽度。
如何加入到打印?
我们可以先观察目前的打印结果和目标结果来确定代码改进方向,下图分别展示了当前代码结果和目标结果:
TreeNode(10000, 20000, 20000, 30000, 40000, 54000, 30000).printNode()
首行空格
blank
分别为 0 -> 3 -> 9 -> 21 -> 45
节点间空格分别为
1 - > 7 -> 19 -> 43
显然,字符的宽度记为
w
与首行空格以及节点间空格有如下关系(当前字符宽度为5
),记当前层级为n
:blank = (w + 1) / 2 * (2 ^ n - 1)
由于字符宽度为偶数时不好取中间值,可以将偶数宽度统一改为奇数:如宽度为4时取宽度为5。所以上面的公式还可以这么写:
blank = (1 + w / 2) * (2 ^ n - 1)
节点间空格的公式没有变化,依旧是
2 * blank + 1
公式加入到代码
class TreeNode(var `val`: Int) {
var left: TreeNode? = null
var right: TreeNode? = null
private fun String.paddingTo(w: Int, c: Char = ' '): String {
val target = if (w % 2 == 0) w + 1 else w
if (this.length >= target) return this
val sb = StringBuilder(this)
while (sb.length < target) {
sb.insert(0, c).append(c)
}
return sb.substring(0, target)
}
fun printNode() {
val queue = LinkedList<TreeNode?>()
val high = getHigh()
val width = getValWidth()
var level = 0
queue.offer(this)
while (queue.isNotEmpty()) {
level++
val size = queue.size
for (i in 0 until size) {
val temp = queue.pop()
val blank = " " * (((1 + width / 2)) * ((1 shl (high - level + 1)) - 1))
val tempVal = "${temp?.`val` ?: "N"}".paddingTo(width)
if (i == 0) {
print("$blank$tempVal")
} else {
print("${blank * 2} $tempVal")
}
if (temp != null) {
queue.offer(temp.left)
queue.offer(temp.right)
}
}
println()
}
}
// 省略部分代码
// ......
}
fun main() {
TreeNode(10000, 20000, 20000, 30000, 40000, 54000, 30000).printNode()
}
// 打印结果
10000
20000 20000
30000 40000 54000 30000
N N N N N N N N
代码分析
为了保证所有节点的宽度都能扩充到相同的宽度,还加入了一个新的方法paddingTo()
,同样以String
的扩展方法的形式给出,并以默认参数的形式给出填充字符。
正确处理代码中的空节点
目前代码中还存在三个较为明显的问题
- 节点中的空节点没有考虑到,导致如果上层有空节点且下层节点有数据,会导致空节点也打印出了左右子树,这明显是有问题的
- 空节点的占位符有点丑,我们需要把它去掉或者支持自定义
- 最后一排空节点是多余的,我们需要去掉它,并同时缩小整棵树
运行下面代码我们可以发现打印结果有明显问题
fun main() {
TreeNode(5, 4, 8, 11, null, 13, 4, 7, 2, null, null, null, 1).printNode()
}
// 打印结果
5
4 8
11 N 13 4
7 2 N N N 1
N N N N N N
// 目标结果
5
4 8
11 N 13 4
7 2 N N N N N 1
按照我们所想的,节点13
下面应该对应两个空节点,且节点13
旁边的节点4
对应空节点以及节点1
分析原因
fun printNode() {
val queue = LinkedList<TreeNode?>()
val high = getHigh()
val width = getValWidth()
var level = 0
queue.offer(this)
while (queue.isNotEmpty()) {
level++
val size = queue.size
for (i in 0 until size) {
val temp = queue.pop()
val blank = " " * (((1 + width / 2)) * ((1 shl (high - level + 1)) - 1))
val tempVal = "${temp?.`val` ?: " "}".paddingTo(width)
if (i == 0) {
print("$blank$tempVal")
} else {
print("${blank * 2} $tempVal")
}
if (temp != null) {
queue.offer(temp.left)
queue.offer(temp.right)
}
}
println()
}
}
在代码第19行 if (temp != null)
我们做了这样的判断,导致每层二叉树并不是满二叉树,从而导致了二叉树的打印错位问题,现将这个条件移除,将空节点也加入队列,那么判断进入循环条件也需要一定的改变:
while (queue.isNotEmpty())
改为while (queue.any { it != null })
即只要队列中存在不为空的节点,则将节点打出来。
二叉树缩小
在之前的分析中,二叉树的最底层全为空的层已经被我们丢掉了,并且我们知道二叉树的大小与层高密切相关,我们只需要将层高减一即可完成二叉树的缩小了,具体修改如下。
// val blank = " " * (((1 + width / 2)) * ((1 shl (high - level + 1)) - 1))
val blank = " " * (((1 + width / 2)) * ((1 shl (high - level)) - 1))
自定义空节点占位符
使用默认参数的形式将占位符传入方法,替换掉源代码中的“N”
即可。
最终代码
import java.lang.StringBuilder
import java.util.*
class TreeNode(var `val`: Int) {
var left: TreeNode? = null
var right: TreeNode? = null
constructor(v0: Int, vararg v: Int?) : this(v0) {
val queue = LinkedList<TreeNode?>()
var index = -1
queue.offer(this)
while (queue.isNotEmpty()) {
val size = queue.size
for (i in 0 until size) {
val temp = queue.pop()
if (temp != null) {
index++
if (index < v.size && v[index] != null) {
temp.left = TreeNode(v[index]!!)
}
index++
if (index < v.size && v[index] != null) {
temp.right = TreeNode(v[index]!!)
}
queue.offer(temp.left)
queue.offer(temp.right)
}
}
}
}
// 层序遍历,并保存在一个数组中,并删除尾部多余null
private fun toArray(): List<Int?> {
val arr = ArrayList<Int?>()
val queue = LinkedList<TreeNode?>()
queue.offer(this)
while (queue.isNotEmpty()) {
val size = queue.size
for (i in 0 until size) {
val temp = queue.pop()
arr.add(temp?.`val`)
if (temp != null) {
queue.offer(temp.left)
queue.offer(temp.right)
}
}
}
while (arr.last() == null) {
arr.removeAt(arr.lastIndex)
}
return arr
}
override fun toString() = toArray().toString()
private fun getHigh(): Int =
1 + (this.left?.getHigh() ?: 0).coerceAtLeast(this.right?.getHigh() ?: 0)
private fun Int.charWidth(radix: Int = 10): Int = this.toString(radix).length
private fun getValWidth(): Int =
maxOf(this.`val`.charWidth(), this.left?.getValWidth() ?: 0, this.right?.getValWidth() ?: 0)
private fun String.paddingTo(w: Int, c: Char = ' '): String {
val target = if (w % 2 == 0) w + 1 else w
if (this.length >= target) return this
val sb = StringBuilder(this)
while (sb.length < target) {
sb.insert(0, c).append(c)
}
return sb.substring(0, target)
}
fun printNode(char: Char = ' ') {
val queue = LinkedList<TreeNode?>()
val high = getHigh()
val width = getValWidth()
var level = 0
queue.offer(this)
while (queue.any { it != null }) {
level++
val size = queue.size
for (i in 0 until size) {
val temp = queue.pop()
val blank = " " * (((1 + width / 2)) * ((1 shl (high - level + 1)) - 1))
val tempVal = "${temp?.`val` ?: char}".paddingTo(width)
if (i == 0) {
print("$blank$tempVal")
} else {
print("${blank * 2} $tempVal")
}
queue.offer(temp?.left)
queue.offer(temp?.right)
}
println()
}
}
operator fun String.times(n: Int) = (1..n).joinToString("") { this }
}
fun main() {
println("*".repeat(50))
TreeNode(5, 1, 4, null, null, 3, 6).printNode()
println("*".repeat(50))
TreeNode(1000, 2000, 2000, 3000, 4000, 5400, 3000).printNode()
println("*".repeat(50))
TreeNode(10000, 2, 20000, 30000, 40000, 54000, 30000).printNode()
println("*".repeat(50))
TreeNode(10000, 20000, 20000, 30000, 40000, 54000, 30000).printNode()
println("*".repeat(50))
TreeNode(100, 200, 200, 300, 400, 500, 300).printNode()
println("*".repeat(50))
TreeNode(1, 3, null, null, 2).printNode()
println("*".repeat(50))
TreeNode(3, 1, 4, null, null, 2).printNode()
println("*".repeat(50))
TreeNode(1, 2, 2, 3, 4, 4, 3).printNode()
println("*".repeat(50))
}
// 打印结果
**************************************************
5
1 4
3 6
**************************************************
1000
2000 2000
3000 4000 5400 3000
**************************************************
10000
2 20000
30000 40000 54000 30000
**************************************************
10000
20000 20000
30000 40000 54000 30000
**************************************************
100
200 200
300 400 500 300
**************************************************
1
3
2
**************************************************
3
1 4
2
**************************************************
1
2 2
3 4 4 3
**************************************************
Process finished with exit code 0
如果你使用的是gradle构建工程,可以直接引入我的LeetCode项目代码
希望这篇文章对你有帮助!使用kotlin完成LeetCode,我与你同行 ^-^ !