记一道算法题解题思路,打印二叉树

455 阅读5分钟

这是我参与更文挑战的第8天,活动详情查看: 更文挑战

前言

万丈高楼平地起,要想路走的远,地基一定要牢固。

但是计算机行业毕竟不是盖房子,我们地基没打好,可以后面再补,但是不管早晚,肯定要补的。

之前两篇文章是字符串解析标题结构字符串解析DOM书,虽然算法的味道淡一些,但是实用性更强。

今天这篇文章是关于打印二叉树的,偏算法,相对我这种初级前端而言,实用性略微低一些。

题目描述

在解题之前,先把题目理清楚,毕竟万丈高楼平地起。

设T为一颗二叉查找树,且T是一颗空树。
然后按顺序将数组A插入到树T中。
实现一个函数。
将这棵树在控制台打印出来。

例:
输入: [9,8,5,0,1,6,2]
输出:
******9******
*****8*10****
***5*****11**
0***6******13
*1***********
**2**********

对应的树状结构如下。

不难看出,打印的结果和最终生成的树的结构不能说是一模一样,只能说是类似。

image.png

解题思路

那么接下来就是分析这道题用到的算法都有什么了?

插入值

首先是插入值,根据结构图可以看出,插入值的规则是判断当前值是否小于或者大于当前值,如果小于就向左继续查找,如果大于就向右继续查找。

不过没有对二叉树进行平衡处理, 这种树称之为二叉查找树。

插入值比较好处理。

获取树层

那么难点来了,如果要打印这棵树,那么肯定是需要知道各个层上有多少值?该值的父级是谁?以及位置是在哪?

虽然在图中可以看到每层有多少个值,以及值的位置,但是转换成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);
}

调用完当前函数之后,生成的结构大概如下。

是不是觉得还是前面的图好懂,转成数据结构就开始变得复杂了?

image.png

第三步、遍历树,生成打印队列。

这一步主要是用广度优先遍历,生成最终需要打印的队列,预先思考结构如下

[[9],[8,10],[5,11]]

但为了在打印的时候,足够封面,所以在生成队列时,丰满了左侧节点。

[[9],[8,10],[5,'*',11]]

对应的图如下,牺牲了一些性能,换来更好一些的打印效果。

image.png

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])

看看打印结果

image.png

第五步、反思

虽然看起来还好,但是其实到了这,我也意识到了一个很严重的问题;

如果子节点开叉的话, 势必会撞车,除非预先空留足够的空间,但是那样要整体移动,才能完成。

所以这一步我就有点麻爪了。 这些问题放在后面思考吧。

总结

整道题采用的是结构是树。

算法用到了插入二叉树、广度优先遍历。

希望看到这里的你,有什么好的思路能给个建议,让我能继续完善他。