在终端中绘制二叉树

简述        

我们可以很轻易地使用C语言构建一颗二叉平衡搜索树,也可以轻易的对其进行遍历或者访问某一个节点,但是总感觉这棵树我们看见了,却没有完全看见,因为通过先序遍历、中序遍历或者后续遍历出来的结果,我们并不能直观的看到这棵树到底长什么样子,想知道某个节点的父节点还需要去仔细分析。有没有可能可以在终端(命令行)中把树绘制出来,网上这样的方法其实很多,但是要么所使用字符需要Unicode编码,不同IDE支持起来有问题,要么就是绘制的结果并不是很好看,有的是把树倒过来水平输出的,这些都不直观。于是我决定自己设计算法,在终端中输出树。

绘制结果

                              11
                              |
              ---------------------------------
              |                               |
              4                               15
              |                               |
      -----------------               -----------------
      |               |               |               |
      2               7               13              17
      |               |               |               |
  ---------       ---------       ---------       ---------
  |       |       |       |       |       |       |       |
  0       3       5       9       12      14      16      18
  |               |       |                               |
  ---             ---   -----                             ---
    |               |   |   |                               |
    1               6   8   10                              19

算法详解        

这里只介绍我的算法是怎么绘制一棵树的,具体树的构建不过多赘述。对于后文将提到的我所定义和使用的函数,可以在文末链接中找到具体实现。

我的目标是将树垂直绘制,即第一行是第一层也就是树根,第二行是第二层以此类推。父亲与儿子节点之间通过 “ | ” 和 “ - ” 连接。由于命令行是逐行输出,而且需要先输出父节点,再输出子节点,那么我们就需要提前知道每一行的具体元素是什么,元素在自己这一行的具体位置,元素间的间隔怎么计算以及怎么确定是否有左右孩子等等。这些问题咱们一个一个解决。

第一,怎么存放这棵树。这里所说的存放其实是为了输出时有一个样板来输出,因为是逐行输出,所以很难用递归的方式访问树的节点并输出,我们就需要先知道树的全貌。我所使用的方法是用一个矩阵存储,其实就是先在矩阵中画出树,再输出到终端。例如前文所展示的绘制结果,可以将其在矩阵中提前存储,其中一部分的存储如下:

[ ] [ ] [ ] [ ] [ ] [ ] [2] [ ] [ ] [ ] [ ] [ ] [ ]
[ ] [ ] [0] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [3] [ ] [ ]
[ ] [ ] [ ] [ ] [1] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ]

这个矩阵中有的格子只用作元素间间隔,有的专门存放元素,哪怕该位置可能没有元素,例如这里给出的例子中,即使0没有左子树,但我们仍然用一个格子代替元素,或者可以说这个地方的元素就是null。        

第二,每一行有哪些元素。这一步很简单,每个结点的高度可以很轻易的获取,只需要对树进行层序遍历即可,这里我的函数为GetNodesOnHeight()。这个函数获取到某一行的节点后将这些节点存放在一个vector中。

第三,确定元素之间的间隔。首先需要定义最下面的一行的间隔,这里我定义为3,也可定义为其他值。每一层的间隔应该是它下一层的间隔的两倍加1,因为每个节点最多有两个子节点,同时还需要给这一层每个结点的父节点留一个位置。如果不好想可以看前面输出结果中2到7的距离就等于0到5的距离。

第四,每一行第一个元素的开始输出位置。例如前文中0的前面就留了两个空格。因为每一行的元素输出需要为后面的所有左子节点预先留出足够的位置。最后一行元素是不需要预留位置的,因为已经没有子节点了。这里需要一个最大宽度的概念。一棵树的最大宽度应该是底层的宽度,也就是底层元素个数和对应所需要的间隔总共占的宽度。每一层的开始位置,应为最大宽度减去这一行最大节点数再减去所有间隔的宽度再除以二,除以二是为了达到左右对称。

第五,确定每一行中的某一个元素在该行中的具体位置。一个简单的例子就是前文输出结果中,最后一行的元素1,虽然看起来是第一个位置,但其实前面预留了一个空节点的位置,所以1应该在2这个位置,这保证了每一行输出的子节点可以和上一行的父节点在位置上对应。那么怎么确定呢?我所采用的方法是利用哈夫曼编码。举个栗子:

 这棵AVL树中最底层元素为0,2,4,6,哈夫曼编码分别为00,01,10,11,如果将这些二进制代码转为十进制则可得到0,1,2,3,也就是最低层元素的具体位置,通过这个方法我们可以确定每个元素在当前这一层的位置。其实仔细想想这种哈夫曼编码确定位置的方法,就体现了二进制的本质。

现在我们其实已经将树存在了矩阵中,且每个元素都在正确的位置上,剩下的就是输出元素,以及输出 “ | ” 和 “ - ” 使得父子关系明确。

输出 “ | ” 的方法就是判断每一个元素是否有子节点,不管是一个子节点还是两个子节点都要输出,没有子节点就不输出,输出 “ - ” 的方法是根据下一行的间隔来输出。这一步其实有很多需要注意的点,而且有的地方蛮难的,但是本文只讲解绘制树的核心思路,就不赘述。如果需要了解输出细节,可以自行阅读代码。

测试用例

#include<iostream>
#include<vector>
#include"BST.hpp"
using namespace std;

int main(){
    vector<int> nums = {0,4,5,3,7,2,9,1,8,6};
    BST* bst = new BST(&nums);
    bst->Insert(10);
    bst->Insert(11);
    bst->Insert(12);
    bst->Insert(13);
    bst->Insert(14);
    bst->Insert(15);
    bst->Insert(16);
    bst->Insert(17);
    bst->Insert(18);
    bst->Insert(19);
    bst->DrawVertical();
    return 0;
}

总结

自己琢磨算法的过程其实很有趣,设计出算法之后也能有一种自豪感。这是我第一次写这种类型的文章,有不妥的地方还望指正。另外讲讲这个算法的缺陷,我第一次在设计算法的时候,没有考虑到如果是大于10的数字其实会占两个字符,这就导致大于10的数字后面会偏移的比较大,解决方法是将大于10的数字后面删除一个空格,但这仍然无法解决例如大于100的数或者更大的数会出现的偏移,另外最底层的间距取值我直接设置为3,但如果数字很大,超过这个范围,依然会出现上下两行之间的位置不匹配问题,这些问题有待改进。

代码:DataStructure/BST.hpp at main · shetell/DataStructure (github.com)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值