打印思路
不管要打印什么,首先要明确的“空格”肯定是在格式化输出起到了不可替代的作用了,所谓的格式化输出也不过是用空格来对做出的一些调整的。我们打印的的树只是在终端上面显示的,他无非是一些数据元素ElemType加上一堆空格。那么我们就要知道什么时候打印空格什么时候打印元素,这点是非常关键的。那么要是想知道这个问题首先就要去观察二叉树的图形,看看二叉树的图形具体有什么可以摸索的规律!
观察二叉树图形(逻辑推理)
先看这个图,我们要想如果打印图1要怎么办呢?这个时候要想什么时候打印空格,什么时候打印回车。对于图1而言,结果显而易见了,首先要打印空格,然是A,回车,B,空格,C。那么问题来了,对于图1,我们一眼看上去就是知道要给A补一个空格,但是对于图2来说,好像看起来是非常困难的,那么我们要怎么知道要补充多少个空格呢?
在这个时候我们有个疑问,为什么要补充空格呢?
假如不补充空格的话,是不是对于图1而言就是一个L的图形啊?
这样可能有人就会说了,只是不美观而已嘛,这只是一个特例,如果出现第三层并且上面的结点多于一个的话我们是无法分辨他是B的孩子还是C的孩子,到底是B的左孩子还是C的左孩子。二叉树的左右子树的严格区分的所以说这点对于我们来说很重要。
所以我们知道了插入空格是为了我们区分结点和结点的关系的。但是,还是没有说明在图2中的A结点前面要插入多少个空格,这里提供一种简单的逻辑思路,插入空格是为了分清楚结点A的孩子,而他的孩子是在下一层所以是插入一个空格,那么结点A的孩子B和C是不是也有孩子呢?那么是不是B和C也要插入空格呢?答案是一定的,依次类推,一直递推到最后一行他一定是不需要插入空格的,为什么?因为他没孩子啊!
那么基于这个推理,马上是不是就知道了在输出结点之前要输出多少个空格呢?那么我们就解决了第一个问题了。
第二个问题是什么时候输出结点呢?
如果是第一个问题已经得到解决了,那么这个问题就解决了,我们只需要在输出了空格之后输出结点
第三个问题是什么时候输出下一个结点和换行?
当我们输出了第一个结点的时候,一个新的问题是我们什么时候输出下一个结点以及在输出下一个结点之前要输出多少个空格呢?这个问题是本质其实还是补充多少个空格的问题对不对?在图2中结点B和结点C之间是有空格的吧,那么这个空格数是不是基于第三层结点C有多少个孩子然后再去补充空格的。
对于在输出完毕一层的全部结点后要输出换行,越高层输出的换行越多,整体成递减关系即可!
我们只要解决了上面的三个问题是不是就可以轻松的解决了这个打印的格式问题。
分析一下三个问题
- 第一个是:要分析每一层的第一个结点的左边要输出多少个空格
- 第二个是:什么时候输出结点
- 第三个是:什么时候输出下一个结点和换行
由第一个问题引发的思考
如何知道每一层在第一个结点的左边要输出几个空格呢?
这个问题是要怎么分析呢?既然是要按照二叉树树的格式输出要体现二叉树的结构,就有了左右孩子的区别了呢? 但是,有的二叉树他有时候只有一个左孩子或者是只有一个右孩子,那要怎么办呢。在这里要吧二叉树想象成一个满二叉树的状态去思考问题,因为二叉树是严格区分做左右孩子的。假设有1层结点我们不需要补空格,有2层结点我们需要补一个空格(来代替左孩子),有3层结点需要4个空格,有4个层就需要7个空格,有5层需要15个空格。这到底是怎么去计算的呢,因为不只是要用空格代替他的左孩子,还有代替左孩子的所有孩子,一直递归下去,这个数是非常恐怖的!经过观察这些数是
2
h
−
1
−
1
2^{h-1}-1
2h−1−1个。
虽然我们解决了第一个问题,但是发现非常的麻烦!回过头想另外俩个问题视乎比第一个问题还要有难度!这时来回顾下我们要干什么事情,要输出一个二叉树对吧?要知道什么时候是输出空格什么时候输出结点对吧?还有别的情况吗?没有,那意味的是不是我们只要知道什么时候输出空格就好了,只要我们知道全部输出的位置 ,那么有办法知道全部输出的位置吗?
在第一个思路上的改良
如果我们知道了全部输出的位置,还知道要输出结点的位置就大功告成了。这里吧全部的位置叫做全集,把空格的位置相对于结点的位置叫做补集
直接观察法
分析它的位置信息
首先,从这个打印的美观这个角度来说,是不是作为一个孩子的双亲结点处于俩个孩子的最中间的位置是最美观的呢? 所以在画图3的时候我是从下往上画的,为什么要从下往上画呢?得先确定了孩子结点的位置,才能确定双亲结点的位置。如果说,双亲结点的位置处于孩子结点位置的中间的话,是不是意味着双亲结点是插入到孩子结点的中间。
我们知道在二叉树中每一层的元素是 2 i − 1 2^{i-1} 2i−1个结点,
那么最后一层的元素个数是多少呢?当然是 2 h − 1 2^{h-1} 2h−1个结点了
那么最后已成需要打印的空格个数是是不是 2 h − 1 − 1 2^{h-1}-1 2h−1−1个呢
现在最后一层总共有多少个呢?就是 2 h − 1 + ( 2 h − 1 − 1 ) = 2 h − 1 2^{h-1}+(2^{h-1}-1)=2^h-1 2h−1+(2h−1−1)=2h−1个位置
而 2 h − 1 2^h-1 2h−1恰好是一个满二叉树的总结点个数,到此,我们找到了一个合理的方案
就是把所有的元素都插空拍到最后一行上面,这个做法是可行的并且也是合理的。
此外,这样打印出来的树相对来说是美观的,因为双亲结点都在孩子结点的中间位置!
图3说到画这个二叉树的时候是要从最后一层开始的,至于这样原因是俩个孩子结点确定一个双亲结点,为什么这么简单要一直强调呢?2个孩子确定1个双亲,是不是可以理解为合二为一,我们不这样去描述这个事情,我们反过来描述是1个双亲结点将2个孩子结点划分开 。在上面我们说过了,把所有的结点都插入到最后一层中。现在我们得到图4:
有人就开始说了,你这不废话嘛?等等别急,我们给一行数从1开始排序,得到图5:
这样好像没什么屁用,或者说我们为什么要排序呢?
大家想想在打印的时候无非就是行和列的关系了,简单地来说,现在得到的这个序列是不是就是一个打印的一个列的序列呢,为什么这样说呢?
好,大家想一想,我们要打印的时候是不是一个二重循环,我们需要打印空格和换行,当然需要一个二重循环了,现在看图3,是不是就很明白了,打印结点的位置就是图5的这个列序列。
但是我们如何确定这些结点的序列呢?我们把图5还原的图3,得到图6:
那么这样我们就得到了这些结点的一个位置序列location数组,我们在前面就知道了一个双亲结点划分了俩个孩子结点,这是不是就是一个二分的过程呢? 不过在这里我们推广一下,因为我们可以把所有的结点放到最后一层上面,那么第一个结点A的位置是不是就是就是最后一层所有位置的一半,同理B的位置就是最后一层一半的一半,那么好,到现在位置我们每一层的开始元素的位置都搞定了,就是上一层元素位置的一半,那么每一层其他的元素位置是多少呢?可以看到C的位置就是B的位置+A的位置,但其实C的位置是在A二分之后A到O的位置的一半,这个距离是不是A到C的长度?
而A到C的位置也就是A到B的长度,为什么?因为二分他们是对称的,而A到B的位置不就是B的H的长度嘛,他不就是B的位置嘛,那么为什么要非这么大力气证明C的位置是B的位置+A的位置呢?之间二分求得不好吗,当然是不好的!越往下面,每一层的元素个数越多,我们现在只需要记录上一层第一个元素的位置就可以通过迭代的方式求得这一层的所有元素,而每一层的第一个元素就是上一个元素位置的一半或者是
2
h
−
i
2^{h-i}
2h−i,
h
h
h是树的高度,
i
i
i是每一层的序号。那么我们现在只需要知道某一棵二叉树b 的高度
h
h
h,就可以得到它的每一个结点位置信息!
分析它的结点信息
那么我们有一个新的问题,我们把一个二叉树变成满二叉树的目的是为了在打印的时候体现它的左右子树严格的这一特点的。可是我们不能保证输入的二叉树b就是一个满二叉树的,也就是说有的结点位置可能是空的,但是我们想一想这有什么问题呢?我们在存储结点信息的时候,如果它是一个空的结点存入一个空格或者是其他的字符啊什么的不就好了?
现在我们来想一下,我们为什么要存储结点的信息呢?(这不废话嘛,当然是为了输出啊)我们前面提到过,我只要知道结点的信息是不是就知道了空格这个补集的信息了。那么这个结点的信息队列一定是非常讲究的 想想都知道我们在输出的时候一定是按照层次遍历的顺序去输出的,话句话说我们不关心证明得到它的方式,只关心得到它最后的次序是什么样的。
而这个层次遍历的次序恰好是满二叉树顺序存储的次序
而满二叉树的的顺序存储有个特点就是如果这个结点元素是
i
i
i的话,它的双亲结点是
i
2
\frac{i}{2}
2i ,它的左孩子是
2
i
2i
2i,它的右孩子是
2
i
+
1
2i+1
2i+1,当然了这里的
i
i
i是数组的下标,这个结点信息数组data[0]这个位置是不要的,为了方便后面的满二叉树顺序存储的表示。有了这个关系我们就没必要用层序遍历的方式去得到这个顺序存储的关系了,我们可以利用非常简单的先根遍历,中序遍历,后跟遍历的递归方式去解决这个问题。
而每一层和每一层的换行这个关系,其实是不明确的,不过只要是一个递减的关系就好了,至于是多少是最佳的状态,因人而异,我这里每一层是用
h
−
i
h-i
h−i个换行来打印的,看着还行。
好了现在我们已经解决了全部的问题了!可以来实现它了!
void TreeToList(BTNode*b,char *&data,int n) //链式存储变成顺序存储,这里采用了先根遍历的方法,要比层
{ //次遍历的方法简单的许多,最后得到满二叉树的层次顺序是关键
data[n] = b->data;
if (b->lchild != NULL)
TreeToList(b->lchild, data, 2*n);
if (b->rchild != NULL)
TreeToList(b->rchild, data, 2*n+1);
}
调用这个函数会帮助我们得到一个满二叉树的顺序存储的一个数组。
void DispBTreeOfDown(BTNode* b){
int h = BTHeight(b);
int sumNode=pow(2.0,h)-1;
int location[sumNode+1]; //这里不是2^h-1就是为了方便后面的操作,下标从1开始
int standoflast=pow(2.0,h); //这里开始默认第一行的位置是2^h,记录上一行的第一个元素的位置
int standofthis;
int top=0; //用于模拟队列的指针,但不用队列的形式,这样更方便,直观
//求得location数组
for(int i=1;i<=h;i++){ //计算b在满二叉树下的结点位置信息
top++;
location[top]=pow(2.0,h-i); //对于每一层的第一个做一个单独的处理
standofthis=standoflast; //更新每一个这一层的一个迭代标准
for(int j=2;j<=pow(2.0,i-1);j++){ //处理每一层除第一元素以为的结点位置
top++;
location[top]=location[top-1]+standofthis;
}
standoflast=standoflast/2; //更新上一行的标准
}
/*用于查看location数组的位置信息
for(int i=1;i<=sumNode;i++){
cout<<location[i];
}
*/
char data[sumNode+1]; //定义数据数组,同样的不要第一个下标为0的位置
char *p=data;
for(int i=1;i<=sumNode;i++){
data[i]=' ';
}
TreeToList(b,p,1);
/*用于查看顺序结构下满二叉树的结点信息
for(int i=1;i<=sumNode;i++){
cout<<data[i];
}
*/
top=1; //二次利用
for(int i=1;i<=h;i++){
for(int j=1;j<=sumNode;j++){
if(location[top]!=j){
cout<<" ";
}
else{
cout<<data[top];
top++;
}
}
for(int k=1;k<=h-i;k++){ //换行的个数
cout<<endl;
}
}
}
这里我们就可以完整的打印一棵倒立的二叉树了!
如果打印的结点不是字符而是小数或者其他的呢
这里我用整体代换的思想,因为打印的是小数的话,它的宽度就不是一个空格能填满的。
但是,这个打印的整体的逻辑结构是不变的,无非就是从一个空格变成了多个空格,是吗?
也就是说我们存储结点信息的时候依然是存储1个空格,记录位置信息的时候依然是按照1个空格的位置去填充,只不过我们在输出的时候遇到1个空格的地方就换成n个空格!这样显然是可行的!下面直接给出全部程序(内附以字符串形式输入二叉树的测试用例):
注:由于作者能力有限,其他结点信息不是字符的情况下,创建二叉树的函数,请自行补充!
#include <stdio.h>
#include <math.h>
#include <iostream> //非格式化输出下,使用c++中的cout输出流更为方便
using namespace std;
#define ElemType char //打印一个字符例如A,B,C,a,b,c等
#define ElemType1 float //打印一个2位数的小数
#define MaxSize 50
typedef struct node
{
ElemType data; //数据元素
struct node *lchild; //指向左孩子节点
struct node *rchild; //指向右孩子节点
} BTNode;
void CreateBTree(BTNode * &b,char *str) //创建二叉树
{
BTNode *St[MaxSize],*p=NULL;
int top=-1,k,j=0;
char ch;
b=NULL; //建立的二叉树初始时为空
ch=str[j];
while (ch!='\0') //str未扫描完时循环
{
switch(ch)
{
case '(':top++;St[top]=p;k=1; break; //为左孩子节点
case ')':top--;break;
case ',':k=2; break; //为孩子节点右节点
default:p=(BTNode *)malloc(sizeof(BTNode));
p->data=ch;p->lchild=p->rchild=NULL;
if (b==NULL) //*p为二叉树的根节点
b=p;
else //已建立二叉树根节点
{
switch(k)
{
case 1:St[top]->lchild=p;break;
case 2:St[top]->rchild=p;break;
}
}
}
j++;
ch=str[j];
}
}
void DestroyBTree(BTNode *&b)
{ if (b!=NULL)
{ DestroyBTree(b->lchild);
DestroyBTree(b->rchild);
free(b);
}
}
int BTHeight(BTNode *b)
{
int lchildh,rchildh;
if (b==NULL) return(0); //空树的高度为0
else
{
lchildh=BTHeight(b->lchild); //求左子树的高度为lchildh
rchildh=BTHeight(b->rchild); //求右子树的高度为rchildh
return (lchildh>rchildh)? (lchildh+1):(rchildh+1);
}
}
void DispBTree(BTNode *b)
{
if (b!=NULL)
{ printf("%c",b->data);
if (b->lchild!=NULL || b->rchild!=NULL)
{ printf("("); //有孩子节点时才输出(
DispBTree(b->lchild); //递归处理左子树
if (b->rchild!=NULL) printf(","); //有右孩子节点时才输出,
DispBTree(b->rchild); //递归处理右子树
printf(")"); //有孩子节点时才输出)
}
}
}
void TreeToList(BTNode*b,char *&data,int n) //链式存储变成顺序存储,这里采用了先根遍历的方法,要比层
{ //次遍历的方法简单的许多,最后得到满二叉树的层次顺序是关键
data[n] = b->data;
if (b->lchild != NULL)
TreeToList(b->lchild, data, 2*n);
if (b->rchild != NULL)
TreeToList(b->rchild, data, 2*n+1);
}
//打印单个字符(像A,B,C,a,b,c,等等)的请用这个函数!
void DispBTreeOfDown(BTNode* b){
int h = BTHeight(b);
int sumNode=pow(2.0,h)-1;
int location[sumNode+1]; //这里不是2^h-1就是为了方便后面的操作,下标从1开始
int standoflast=pow(2.0,h); //这里开始默认第一行的位置是2^h,记录上一行的第一个元素的位置
int standofthis;
int top=0; //用于模拟队列的指针,但不用队列的形式,这样更方便,直观
//求得location数组
for(int i=1;i<=h;i++){ //计算b在满二叉树下的结点位置信息
top++;
location[top]=pow(2.0,h-i); //对于每一层的第一个做一个单独的处理
standofthis=standoflast; //更新每一个这一层的一个迭代标准
for(int j=2;j<=pow(2.0,i-1);j++){ //处理每一层除第一元素以为的结点位置
top++;
location[top]=location[top-1]+standofthis;
}
standoflast=standoflast/2; //更新上一行的标准
}
/*用于查看location数组的位置信息
for(int i=1;i<=sumNode;i++){
cout<<location[i];
}
*/
char data[sumNode+1]; //定义数据数组,同样的不要第一个下标为0的位置
char *p=data;
for(int i=1;i<=sumNode;i++){
data[i]=' ';
}
TreeToList(b,p,1);
/*用于查看顺序结构下满二叉树的结点信息
for(int i=1;i<=sumNode;i++){
cout<<data[i];
}
*/
top=1; //二次利用
for(int i=1;i<=h;i++){
for(int j=1;j<=sumNode;j++){
if(location[top]!=j){
cout<<" ";
/*如果打印哈夫曼树的结点是两位小数时,使用这个打印4个空格
for(int t=0;t<4;t++){
cout<<" ";
}*/
}
else{
/*在打印哈夫曼树时,如果这个结点是空结点就要输出4个空格而不是1个空格
if(data[top]==' '){
for(int t=0;t<4;t++){
cout<<" ";
}
}
else{
cout<<data[top];
}*/
cout<<data[top];
top++;
}
}
for(int k=1;k<=h-i;k++){ //换行的个数
cout<<endl;
}
}
}
/*
测试函数1
int main(){
BTNode* b;
CreateBTree(b, (char*)"A(B(C,D),E(,F))");
DispBTree(b);cout<<endl;
DispBTreeOfDown(b);cout<<endl;
DestroyBTree(b);
system("pause");
return 0;
}
*/
/*测试函数2,
用于寻找一个小数,字符,任何可以键入的东西和空格的关系
这里测试得知一个2位小数等于4个空格,这里要注意所运行代码的环境的不同
可能在终端显示的不太一样,请测试后自行去修改数据
int main(){
while(1){
cin.get();
}
return 0;
}
测试方法举例:
***注意***:一定要在输出的终端测试,在这里测试的数据可能不准确!!!
A G
B //所以1个英文字母是1个空格的宽度
1.00 0.23
4.00 //所以1个2位小数是4个空格的宽度
*/
/*
如果要打印哈夫曼树,
最后一行的位置一共是[2^{h-1}(结点数)+4*(2^{h-1}-1)(空格数)]=5*2^{h-1}-4
但是真的这样考虑吗?
这样去思考,所有的东西都没变,变的只是结点打印出来的宽度,
只需要把之前打印出来的空格看成一个整体,一次性打印4个空格是不是就好了?
所以公式依然不边,凡是涉及到一个空格的情况,先判断是不是打印哈夫曼树,然后输出4个空格。
但是这里建议直接给一个float数组的满二叉树形式,如果是给字符数组的形式将会非常麻烦!
也就是直接输入一个满二叉树的float的形式,去测试!
*/