这是我参与更文挑战的第8天,活动详情查看: 更文挑战
前言
万丈高楼平地起,要想路走的远,地基一定要牢固。
但是计算机行业毕竟不是盖房子,我们地基没打好,可以后面再补,但是不管早晚,肯定要补的。
之前两篇文章是字符串解析标题结构、字符串解析DOM书,虽然算法的味道淡一些,但是实用性更强。
今天这篇文章是关于打印二叉树的,偏算法,相对我这种初级前端而言,实用性略微低一些。
题目描述
在解题之前,先把题目理清楚,毕竟万丈高楼平地起。
设T为一颗二叉查找树,且T是一颗空树。
然后按顺序将数组A插入到树T中。
实现一个函数。
将这棵树在控制台打印出来。
例:
输入: [9,8,5,0,1,6,2]
输出:
******9******
*****8*10****
***5*****11**
0***6******13
*1***********
**2**********
对应的树状结构如下。
不难看出,打印的结果和最终生成的树的结构不能说是一模一样,只能说是类似。
解题思路
那么接下来就是分析这道题用到的算法都有什么了?
插入值
首先是插入值,根据结构图可以看出,插入值的规则是判断当前值是否小于或者大于当前值,如果小于就向左继续查找,如果大于就向右继续查找。
不过没有对二叉树进行平衡处理, 这种树称之为二叉查找树。
插入值比较好处理。
获取树层
那么难点来了,如果要打印这棵树,那么肯定是需要知道各个层上有多少值?该值的父级是谁?以及位置是在哪?
虽然在图中可以看到每层有多少个值,以及值的位置,但是转换成JS能理解的对象或者数组的时候,并不会这么清晰,所以是需要进行代码干预的。
在这一块我用到了广度优先遍历记录每一层的值,为的就是最后一步打印的时候,能确定他的位置。
第一步、定下主函数
为了便于理解,我会用完善函数的行为,丰富主函数。
同时为了不水字数,我会在不影响理解的前提下,在完善函数的地方,删除其他不必要的代码及注释。
感兴趣小伙伴一起来敲啊,加深理解。我不会在某个地方放出完整的代码的,除非你一个个的复制下来。
通常来说,主函数要做的事情比较简单,负责调用其他函数,但是这里打印树会用到引用类型传参,所以在主函数中,会先定义好最终存储结果的几个引用类型。
/**
* @param {Number|String} node 当前节点值
* @param {Number} level 当前节点层级
* return {node: number, left: null, right: null} 输出的节点对象
*/
class Node{
constructor(node, level){
this.node = node;
this.left = null;
this.right = null;
this.level = level;
}
}
/**
* @param {Array} array 需要生成并打印树的数组
*/
function printTree(array){
let tree, queue = [], // queue 存储需要打印的队列
count = {left: 0, right: 0, deep: 0}; 记录树的深度与宽度
tree = new Node(array.shift(),1);
// 数组为空、或单个值,直接返回不打印
if(!array.length) return false;
for(let item of array) insertTree(item, tree);
return tree;
}
为了避免一次写太多,让萌新看着头大,入口函数先就这么多,先去完善插入函数insertTree() 。
第二步、插入值函数生成二叉树
这一步比较简单, 不多说,一些疑惑点会在代码中注释。
function insertTree(data, tree, count){
/* 用current引用一边tree,让tree保持在根节点,而current用于向下插入值。
否则在向下插入值的时候,会导致tree不在为根节点,导致值错误。
有不理解的小伙伴,可以搜一搜了解下引用类型哦。
*/
let current = tree, level = 1;
left = right = deep = 0; // 记录本次插入时的宽度,与深度
while(current){
deep++;
level++;
if(data <= current.node){ // 这里用小于等于是为了避免值和节点相等的情况,会进入死循环
left++; right = 0; // 拐弯的时候,清楚另一边,避免误算
if(current.left){
current = current.left;
} else { // 有值继续向下,没值就插入值
current.left = new NOde(data, level)
break;
}
} else if( data > current.node){
right++; left = 0;
if(current.right) {
current = current.right;
} else {
current.right = new Node(data, level);
break;
}
}
}
count.deep = Math.max(count.deep, deep);
count.left = Math.max(count.left, left);
count.right = Math.max(count.right, right);
}
调用完当前函数之后,生成的结构大概如下。
是不是觉得还是前面的图好懂,转成数据结构就开始变得复杂了?
第三步、遍历树,生成打印队列。
这一步主要是用广度优先遍历,生成最终需要打印的队列,预先思考结构如下
[[9],[8,10],[5,11]]
但为了在打印的时候,足够封面,所以在生成队列时,丰满了左侧节点。
[[9],[8,10],[5,'*',11]]
对应的图如下,牺牲了一些性能,换来更好一些的打印效果。
function generateTree(tree, queue, deep){
let result = [tree], // 用于遍历的起点。
cDeep = 0; // 当前遍历的深度
while(result.length){
let level = result.length;
let currLevel = [];
cDeep++;
for(let i = 0; i < level; i++){
let curr = result.shift();
if(cDeep <= deep){
// 丰满左侧节点,输出时更美观
typeof curr.left?.node !== 'number' && (curr.left = new Node('*', cDeep));
}
curr.left = result.push(curr.left) : '';
curr.right = result.push(curr.right) : '';
currLevel.push(currnode);
}
queue.push(currLevel);
}
}
第四步、完善主函数
function printTree(array){
// ...略
for(let item of array) insertTree(item, tree);
generateTree(tree, queue, count.deep); // 生成需要打印的队列;
// 树和队列已经生成好了,开始搞事情吧
for(let item = 0; item <= count.deep; item++){
let start = '*'.padStart(count.deep - item, '*'); // 填充在前面的符号
let end = '*'.padEnd(count.deep - item, '*'); // 填充在后面的符号
let value = queue[item].join('*');
console.log(start+value+end)
}
return tree;
}
printTree([9,10,11,12,8,5,0,1,6])
看看打印结果
第五步、反思
虽然看起来还好,但是其实到了这,我也意识到了一个很严重的问题;
如果子节点开叉的话, 势必会撞车,除非预先空留足够的空间,但是那样要整体移动,才能完成。
所以这一步我就有点麻爪了。 这些问题放在后面思考吧。
总结
整道题采用的是结构是树。
算法用到了插入二叉树、广度优先遍历。
希望看到这里的你,有什么好的思路能给个建议,让我能继续完善他。