NewbieX-二叉树的图形化显示

13 篇文章 3 订阅

这个是 NewbieX 的个人代码练习第二篇,之前一篇在这里,本文就用来实现一个二叉树结构的图形化显示,具体就是把一个二叉树打印出来到命令行终端,可以满足平时自己的代码练习,也就没打算再继续升级了,反正够用就行。目前支持的特性/缺点有下面几个:

  1. 满二叉树、非满二叉树的打印。
  2. 对节点的字符长度要求较为宽松,不必要求等长的节点字符长度。
  3. 无法最大限度利用命令行终端的空间,会有浪费。
  4. 代码不怎么考虑内存消耗,反正完成任务就行。
  5. 对输入格式要求比较严格,必须要先转换成数组形式的存放方式,数组内容是每一个二叉树节点的值的地址。

一个基本的例子如下所示:
示例

关于 NewbieX

自己建了一个 github 小代码仓库来存放自己用来练习的 C/C++ 代码,名字就随便起了个名字叫做 Newbie-X,X 呢在很多意义上都是神秘的代名词,没有什么具体的意义,很多项目都喜欢用 X 作为一个后缀什么的,这里就效仿一波拿来取了个名字。里面放了一些 C++、数据结构与算法、图形学、Buffer 管理小工具等等东西。作为个人的代码工具库外加代码编写练习,毕竟代码要常写才不会手生。链接: https://github.com/YellowMax2001/Newbie-X:

放一个代码仓库的链接: https://github.com/YellowMax2001/Newbie-X ,阅读原文里面也链接到这里了。

基本思路

最开始的时候肯定是先去网上找一找看有什么新鲜想法,然后大概结合了满二叉树的画法和非满二叉树的画法,最后自己按照自己的想法结合了网上的思路,最终形成下面的设计思路。

输入输出

输入:一个二叉树的数组表现形式,把空节点作为 NULL 类型的指针存储到数组里面,可想而知数组里面存放的是二叉树的节点的值的地址。

  1. 二叉树必须要有一个可供打印的 INT32 类型的值(value),我们画二叉树的时候只画这个值。
  2. 二叉树可以任意类型,但是存进数组里面必须按照满二叉树的方式存放,空的节点就放 NULL 进去数组里面。

输出:图形化的二叉树结构,如文章开头的图所示。

设计

  1. 二叉树要向下扩展,就是跟鱼骨形的思维导图一样,生长方向是向下,这样符合我们视觉感受并且降低了输出到命令行的难度。
  2. 要确定每一个节点的打印坐标,这里我分了两个坐标,x, y,x 是同一层的节点的值起始坐标,就是命令行的打印坐标,y 是二叉树的层。
  3. 动态分配二叉树的暂存数据结构,用于最终的图像化打印显示。

实现方式分为两个过程,第一步就先实现一个满二叉树的打印,也就是说没有空节点,然后再实现任意二叉树的打印,任意二叉树如果有空节点那就是满二叉树的一种特例,后面可以看出来。

我这里定坐标是选择从最后一层开始,因为对于满二叉树来讲,正常情况下最后一层的最左边节点一定是最突出的,先定好这个坐标,上层的坐标就好处理了,现在先看下类似下面的二叉树结构:
基本例子

  1. 第一步先定好 node2 的坐标,假设最左边的第一条红色竖线就是命令行的最左边,那么 node2 的坐标就是 [0,1],因为我们定打印坐标定的是节点值的最左边,节点 2 的值的长度可以看到是 4 个小方格子的宽度,这个是可以通过把 INT32 的值转换为 string 类型来获取的。
  2. 然后我们再去找 node3 的坐标,node3 的起始坐标 x 应该等于 x3 = x2 + LengthOfx2 + 2. 也就是节点 2 的 x 坐标加上节点 2 的长度再加上我自己定义的节点间隔 2.
  3. 下面两个坐标就找好了,接下来回到最上层的 node1,node1 最重要的是确定其 x 坐标,基本思路是把 node1 的中心点对齐到 node2 和 node3 中心点的中值,也就是图中 3,4 红色竖线的中心,这个公式很简单,也不用多说了。

对于三个节点的我们可以这么搞,那更多的节点呢?就需要进行遍历获取每一个点的坐标,我们从最后一层的左子节点开始,一直往回遍历到第一层,每一个节点都按照上面说的方法来进行坐标定位,不同的节点会有不同的处理方式:

  • 没有叶子节点:就是上图中的 node2,node3,这种情况就适用于上述的第一步,右边的节点等于左边的节点加上中间偏移值。
  • 有叶子节点:适用于上述第三步,通过两个子节点的坐标来定位当前节点的坐标。

上面的步骤需要我们怎么去组织数据结构呢?因为我们用到了一种类似于广度优先的遍历方式,上面可以看出,节点的访问顺序是 node2,node3,node1.而这种访问顺序适合用数组来存储,这样我们就可以按照数组顺序来进行顺序访问了,想下满二叉树的数组形式存储,完全二叉树的数组形式存储。所以我们需要一个长度等于二叉树节点数的数组,数组的内容怎么组织呢?我这里在开发的时候想到的,每个成员的含义如注释所述:

struct BTGraphicNode {
    INT32           nodeValue;          // This is binary tree's node value
    Coordinate2D    coorOfNodeRoot;     // This is a 2D coordinate, use x, y description
    INT32           lengthOfNodeRoot;   // Node value should be converted into string, this is regarded as string length
    INT32           treeLevel;          // Which level is this node?
    INT32           leafNodeIndex;      // The leaf node index who are in same tree level
    BOOL            bIsValid;           // Is this a valid non-null node?

    BTGraphicNode   *leftBTSubNode;     // Left sub-node
    BTGraphicNode   *rightBTSubNode;    // Right sub-node
};

所以我们的每一个节点都是上面的数据结构类型,然后把节点串起来,连好左子树右子树等等。

代码实现

首先需要遍历数组把二叉树的节点值赋值给我们内定的数据结构:

INT32 nodeLevelStartIdx = 2;
INT32 nodeLevelEndIdx   = 0;
INT32 nodeLevelIdx      = 0;
INT32 nodeTreeLevelIdx  = 0;

// Forward traverse to fill main member of binary graphic tree and build binary graphic tree.
for (INT32 nodeIdx = 0; nodeIdx < BTArrayNodeCount; nodeIdx++)
{
    INT32 leftSubNodeIdx    = nodeIdx * 2 + 1;
    INT32 rightSubNodeIdx   = leftSubNodeIdx + 1;

    // 这里说明已经是最后一层了,所以没有子树
    if (leftSubNodeIdx > BTArrayNodeCount - 1)
    {
        tmpBTGraphicNode[nodeIdx].leftBTSubNode     = NULL;
    }
    else
    {
        tmpBTGraphicNode[nodeIdx].leftBTSubNode     = &tmpBTGraphicNode[leftSubNodeIdx];
    }

    if (rightSubNodeIdx > BTArrayNodeCount - 1)
    {
        tmpBTGraphicNode[nodeIdx].rightBTSubNode     = NULL;
    }
    else
    {
        tmpBTGraphicNode[nodeIdx].rightBTSubNode     = &tmpBTGraphicNode[rightSubNodeIdx];
    }

    // 每次遇到一层的起点,也就是每一层的最左边子树,就重置变量
    if ((nodeIdx + 1) == nodeLevelStartIdx)
    {
        nodeTreeLevelIdx++;
        nodeLevelIdx = 0;
        nodeLevelStartIdx = nodeLevelStartIdx * 2;
        NEWBIE_INFO(GRAPHIC_LOG_GRP, "Got Tree level number %d", nodeTreeLevelIdx);
    }
    tmpBTGraphicNode[nodeIdx].treeLevel         = nodeTreeLevelIdx;
    tmpBTGraphicNode[nodeIdx].leafNodeIndex     = nodeLevelIdx++;

    // 如果节点为非空,就赋值
    if (NULL == ppBTArray[nodeIdx])
    {
        tmpBTGraphicNode[nodeIdx].nodeValue         = 0;
        tmpBTGraphicNode[nodeIdx].lengthOfNodeRoot  = 0;
        tmpBTGraphicNode[nodeIdx].bIsValid          = 0;
    }
    else
    {
        tmpBTGraphicNode[nodeIdx].nodeValue         = *ppBTArray[nodeIdx];
        tmpBTGraphicNode[nodeIdx].lengthOfNodeRoot  = std::to_string(*ppBTArray[nodeIdx]).length();
        tmpBTGraphicNode[nodeIdx].bIsValid          = 1;
    }
}

上面是第一遍遍历进行赋值的过程,做了一下几个事情:

  1. 构建一个自定义的二叉树,树的节点保存自己感兴趣的内容。
  2. 把节点 value 填充好,并计算出来节点的长度等等。
  3. 填充当前节点所在的层。
// Backward traverse to fill the node print coordinates.
// for loop is level by level backward
nodeLevelStartIdx = nodeLevelStartIdx - 1;
nodeLevelEndIdx   = nodeLevelStartIdx;
while (nodeLevelStartIdx > 0)
{
    nodeLevelStartIdx = nodeLevelStartIdx / 2;
    NEWBIE_INFO(GRAPHIC_LOG_GRP, "nodeLevelStartIdx %d, nodeLevelEndIdx %d",
        nodeLevelStartIdx, nodeLevelEndIdx);

    for (INT32 i = nodeLevelStartIdx; i < nodeLevelEndIdx && i < BTArrayNodeCount; i++)
    {
        if (HasFullLeafNode(&tmpBTGraphicNode[i]))
        {
            tmpBTGraphicNode[i].coorOfNodeRoot.xCoor = 
                tmpBTGraphicNode[i].leftBTSubNode->coorOfNodeRoot.xCoor +
                (tmpBTGraphicNode[i].rightBTSubNode->coorOfNodeRoot.xCoor -
                tmpBTGraphicNode[i].leftBTSubNode->coorOfNodeRoot.xCoor) / 2;
            tmpBTGraphicNode[i].coorOfNodeRoot.xCoor -= 
                tmpBTGraphicNode[i].lengthOfNodeRoot / 2;                
        }
        else if (NULL != tmpBTGraphicNode[i].leftBTSubNode)
        {
            tmpBTGraphicNode[i].coorOfNodeRoot.xCoor = 
                tmpBTGraphicNode[i].leftBTSubNode->coorOfNodeRoot.xCoor +
                tmpBTGraphicNode[i].leftBTSubNode->lengthOfNodeRoot + 1;
            tmpBTGraphicNode[i].coorOfNodeRoot.xCoor -= 
                tmpBTGraphicNode[i].lengthOfNodeRoot / 2;
        }
        else if (NULL != tmpBTGraphicNode[i].rightBTSubNode)
        {
            tmpBTGraphicNode[i].coorOfNodeRoot.xCoor = 
                tmpBTGraphicNode[i].rightBTSubNode->coorOfNodeRoot.xCoor - 1 - tmpBTGraphicNode[i].lengthOfNodeRoot;
            tmpBTGraphicNode[i].coorOfNodeRoot.xCoor -= 
                tmpBTGraphicNode[i].lengthOfNodeRoot / 2;
        }
        else
        {
            // Completely leaf node, just based on last leaf node's coordinate.
            if (i > nodeLevelStartIdx)
            {
                tmpBTGraphicNode[i].coorOfNodeRoot.xCoor = 
                    tmpBTGraphicNode[i-1].coorOfNodeRoot.xCoor + tmpBTGraphicNode[i-1].lengthOfNodeRoot + 2;
            }
        }
        if (tmpBTGraphicNode[i].coorOfNodeRoot.xCoor < 0)
        {
            tmpBTGraphicNode[i].coorOfNodeRoot.xCoor = 0;
        }
        tmpBTGraphicNode[i].coorOfNodeRoot.yCoor = nodeTreeLevelIdx;
    }
    nodeTreeLevelIdx--;
    nodeLevelEndIdx = nodeLevelStartIdx;
}

上面是第一次后向遍历,目的只有一个,那就是为了计算出来每一个 node 的打印起始坐标值,基本的坐标逻辑如下所示:

========______3_______
=======/==============\
====___6_____========_17___
===/=========\======/======\
==15_=========13====1_======6
=/===\=======/=\===/==\====/=\
=12===9_=====1==2==7==10==19==3

其中空格为了显示出来就用了 = 符号进行代替,所以上面的基本逻辑是按照行进行打印的,打印的内容我用了下面几个变量去存储:

struct LinkFillChar {
    CHAR LinkFillType;  // 连接线的字符,也就是上面的 "_"
    CHAR BlankFillType; // 空白处的字符,也就是上面的 "=",实际一般设置为空格字符 " "
    CHAR LeftFillType;  // 左子节点和父节点连接线的字符,也就是上面的 "/"
    CHAR rightFillType; // 右子节点与父节点连接线的字符,也就是上面的 "\"
};

剩下的就是把它打印出来了,这个过程是从前往后的,先打印第一行,然后依次往后面进行,具体的代码就可以在 github 链接里面看到:https://github.com/YellowMax2001/Newbie-X ,阅读原文也可以看到。

End

写这段代码的时候顺便把编译的系统更新了下,添加了下面几个支持:

  1. 支持单独编译一个 C、C++ 文件工程:https://github.com/YellowMax2001/Newbie-X/blob/master/MaxCLib/data_structs/binary_tree/Compile.mk
  2. 支持单独编译一个静态库、动态库:https://github.com/YellowMax2001/Newbie-X/blob/master/MaxCLib/data_structs/fifo/ring_buffer/Compile.mk
  3. 支持添加头文件依赖,也就是说改动头文件,相关的 C/CPP 文件也会被重新编译。具体就是在 Compile.mk 里面添加:CompileHeaderFiles += $(addprefix $(call my_dir)/, $(wildcard *.h)),可以选择赋值自己需要添加的头文件依赖。

编译步骤:

  1. 根目录下:source build_tools/newbie_build.src
  2. cd MaxCLib/data_structs/binary_tree/
  3. mMake.sh

目前来看基本上这个编译系统凑合够用,够满足我日常的 coding 需求,后面有需要更新了再去开发新的功能,现在就先这么用着。


  • 2
    点赞
  • 0
    评论
  • 1
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值