二叉树的图形直观显示的初步实现

学习到了二叉树,认识到二叉树太有作用了。但每一次创建了二叉树,总想直观看一下二叉树的结序,以方便程序的调试。但二叉树的结构怎样才能直观显示出来呢?看了网上的一些资料和做法,要么看不懂,要么不满意。于是试着自己写代码,看能否达到目的。功夫不负有心人,经过不懈努力,终于弄出一个初步的结果。兴奋!!!本想进一步完善,添上一些符号把节点连起来再发出来,实在高兴忍不住了……哈哈!!!先上结果的图片:

二叉树的图形直观显示的初步实现

下面简单说一下思路,欢迎大家指正并提出更好的办法。

基本思路:二叉树两条分支,从形状上来讲,左右是对称的。虽然不一定每个节点都有左右两个子节点,但是要直观显示时,位置分布上也要把空节点留出相应的空间来。这样,它始终是对称的。因此:我的方法就是“对称输出”,每个节点,包括空的节点,留出相应位置空间。

基本方法:思路虽然有了,但实现起来很困难。首先每个节点的信息有限,如果只有data,LChild,RChild等信息,就无法了解节点与其它同层节点以及父节点之间的信息与关系,输出时就无法控制了。

所以我的第一步:重新定义一个“节点”的结构,使它包括:父节点指针、深度(层次)、位置序号等成员数据,以上所说的难点就从定义上解决了。下面是节点的结构定义:

template<typename T>
struct BiTNode {
	BiTNode<T> *Lchild = NULL, *Rchild = NULL, *parent = NULL;
	T data;
	int depth = 0;
	int order = 0;
};

节点定义了,怎么在程序中实际生成这些数据呢?因为二叉树要直观图形输出,必须要按层的顺序了解足够的信息。所以首先要能够按层遍历二叉树的方法。这个不难,我还是用利用队列(Queue)的办法解决它。下面是我根据已存在的二叉树,按照自己定义的节点结构重新创建(即复制二叉树)的二叉树的方法代码:

//复制二叉树,按BiTNode结构类,添加深度、序号以及父节点等信息
template<typename T>
BiTNode<T> *CreateBiTTreeFrom(BiTNode<T> *p) {
	//复制p为根结点的树,并添加BiTNode相关变量信息。
	Queue<BiTNode<T> *> qbit;
	BiTNode<T> *root=NULL,*bitNode, *childNode;
	if (p) {
		//根结点复制
		root = new BiTNode<T>;
		root->data = p->data;
		root->depth = 1;
		//ordero为节点序号。节点序号编排原则:从0开始;空节点也要占据相应位置。非实际节点序号。
		root->order = 0;
		root->Lchild = p->Lchild;
		root->Rchild = p->Rchild;

		qbit.QInsert(root);
		//其它节点的复制循环,利用队列先进先出特点,按层序复制所有节点。
		while (!qbit.QEmpty()) {
			bitNode = qbit.QDelete();
			//根据父结点,创建左子节点并添加相关信息。
			if (bitNode->Lchild) {
				childNode = new BiTNode<T>;
				childNode->data = bitNode->Lchild->data;
				childNode->depth = bitNode->depth + 1;
				childNode->parent = bitNode;
				//根据父节点的序号确定上一层元素的个数,从而可以确定当前节点在当前层的序号。
				//序号计算要包括父结点中不存在的左或右子节点的数量。序号不是指实际存在的同层节点的序号。
				childNode->order = childNode->parent->order * 2;
				childNode->Lchild = bitNode->Lchild->Lchild;

				childNode->Rchild = bitNode->Lchild->Rchild;

				bitNode->Lchild = childNode;
				qbit.QInsert(bitNode->Lchild);
			}
			//根据父结点,创建右子节点并添加相关信息。
			if (bitNode->Rchild) {

				childNode = new BiTNode<T>;
				childNode->data = bitNode->Rchild->data;
				childNode->depth = bitNode->depth + 1;
				childNode->parent = bitNode;
				childNode->order = childNode->parent->order * 2 + 1;

				childNode->Lchild = bitNode->Rchild->Lchild;

				childNode->Rchild = bitNode->Rchild->Rchild;

				bitNode->Rchild = childNode;
				qbit.QInsert(bitNode->Rchild);
			}

		}

	}
	return root;
}

第二步:根据新的二叉树,直观输出二叉树。这个非常麻烦,但经过几番探索,终于找到一点二叉树节点空间分布的规律了。下面以图为例说明:

二叉树的图形直观显示的初步实现

解释一下我的想法:1,二叉树的输出如果是对称输出的话,其宽度是由最深一层(最大一层)的节点数量决定的。最深一层可能存在的节点数量很容易由公式计算出来:2^(depth-1),其中depth代表层次的变量。如果最深一层的输出,节点之间(节点值输出本身有一定宽度)不留空白的话,那二叉树输出最大宽度就确定了:maxwidth=deltawidth*最深一层节点数。这里,节点数包括不存在的空节点。deltawidth是每个节点自身输出的宽度,如上图中每个矩形的宽度。有了这个衡量的标准,其它各层的输出也总是对称输出的,就有办法解决了。

2,怎么控制节点输出时的缩进以及节点之间的间隔呢?虽然最深一层节点之间间隔宽度为0,但同层节点之间的间隔在其它层里面,是存在的,也必须存在,不然无法对称,也无法显示出父节点与子节点之间的错落关系。最终我发现:节点以指定宽度输出并保持对称的话,节点之间的空白宽度是有规律的,可以计算出来:

distance=(2^(treedepth-depth)-1)/2*deltawidth。

其中,distance代表二叉树同一层的任意相连的两个节点(空节点就当它存在)之间的“单位”间隔。所谓“单位”间隔,是指两个节点之间的间隔距离是distance的2倍。1个distance又是指什么呢?它刚好是每层第一个节点的缩进宽度。而这个宽度的2倍,又刚好是后面任意两个相连节点之间的间隔。treedepth是整个二叉树的深度(最大层数),depth代表某一层的层数。deltawidth是节点值输出的指定宽度,比如:deltawidth=4。代表4个字符的宽度。从公式来看,节点之间的间隔与层数有关,不同层元素之间的间隔不一样(这很容易理解:层越接近根结点层,节点越少,间隔自然更多)。虽然不同层的间隔不一样,但可以根据层数计算出来,而且:这个间隔在每一层刚好都是distance的两倍。如上图所示。根结点只有一个元素,所以其左右空白在maxwidth内,刚好各是2个distance。找到这个规律,节点之间的间隔就能控制了。

3,怎么控制实际存在的节点之间间隔呢?因为同层中的节点,并不是完全存在的,也不可能有规律哪些节点存在,哪些不存在。这就用到了节点定义中的order成员数据了。order表示同一层中所有可能存在的节点(节点总数根据层数可以计算出来)的顺序号,第一个序号为0,最右边的节点为:节点总数-1。如二叉树第4层,它最多有8个节点,order就是0-7。根据这个节点顺序值,可以计算同层节点输出宽度了:

totalwidth += (order - _preOrder)*(deltawidth + 2 * distance) - 2 * distance;

其中,order,_preorder代表输出时前后两个节点的序号。totalwidth代表输出某个节点时,实际输出的宽度值。

怎样理解这个公式呢?想象一下:节点之间的距离为2*distance,节点本身的输出宽度为deltawidth(根据节点值的字符长短,自己定义的宽度)。所以:任意一个节点输出的宽度加上间隔的话,就是:deltawidth + 2 * distance。但是,很可能存在一些特殊情况:

(1)上一节点是上一层的最后一个实际存在的节点,下一节点不是本层第一个节点(同层第一个节点或连续几个节点不存在)。如果只输出以上宽度的话,对称无法实现的,需要把不存在的节点的宽度和它后面的空白间隔考虑进去。而间隔与节点在同层节点中的序号相关。多一个序号差,就多一个deltawidth + 2 * distance的间隔。前一节点的序号由于是上一层的,不能使用,算作0的话,计算出的结果又是错误的,实践证明:把上一节点的序号改为-1就行了。但如果是同层的,序号就不变。

//存在每层第一个存在的元素的序号不是为0的情况,统一把它第一个元素的序号设置成-1,
					//方便计算元素之间的间距(不一定是序号相连的两个元素)
				if (pre->depth != depth) {
					_preOrder = -1;
				}
				else {
					//不是第一个的元素的序号仍然为原序号。
					_preOrder = pre->order;
				}

(2) 还有一个情况:上述这样计算之后,每个节点后面,输出时都加上了间隔宽度。输出时,一般都是右对齐的,多了些宽度,输出位置就发生错位了。所以要减少这个间隔,即:-2*distance。

如此,输出宽度就解决了。二叉树直观图形式的显示就基本实现了。当然,父结点与子结点之间还没有加上连结的字符,还不是完全的图形输出。基本思考了一下,应该可以实现的,下一步就去做这个事,完成之后就发上来大家指正。

下面是我的完全代码,包括两个模板类:BitTNoe 和 Queue。其中,创建二叉树时,用的是一个递归,根据一个数组的值创建的。其中,根结点的左子节点只有一个(没有办法,为了避免手工创建二叉树的麻烦只能这样了)。

#include<iostream>
#include<iomanip>

using namespace std;

//************************************struct BiTNode****************************************************
template<typename T>
struct BiTNode {
	BiTNode<T> *Lchild = NULL, *Rchild = NULL, *parent = NULL;
	T data;
	int depth = 0;
	int order = 0;
};
//*****************************************************************************************


//************************************class Queue*****************************************************
//队列内元素最大个数:
static const int MAXSIZE=100;

/*模板队列类
*/

template<typename T>
class Queue {
private:
	//队首,指向最先出队列的元素位置。
	int front;
	//队尾,指向下一次插入的新元素位置。
	int rear;
	//已经插入队列的元素个数。
	int count;
	//队列内保存元素的数组。
	T qList[MAXSIZE];
public:
	//构造函数
	Queue();
	//插入元素的函数
	bool QInsert(T& item);
	//弹出元素的函数
	T QDelete(void);
	//清空队列
	void QClear(void);
	//返回正在排队的元素个数。
	int QGetCount(void);
	//队列状态函数:是否为空。
	bool QEmpty();
	//队列状态函数:是否已满。
	bool QFull();
	//析构函数
	~Queue();
};

template<typename T>
Queue<T>::Queue() :front(0),rear(0),count(0){
}

template<typename T>
bool Queue<T>::QInsert(T& item){
	if (QFull()) {
		cout << "队列已满!插入失败!" << endl;
		return false;
	}
	else {
		count++;
		qList[rear] = item;
		rear = (rear + 1) % MAXSIZE;
		return true;
	}
}



template<typename T>
T Queue<T>::QDelete(void){
	
	if (QEmpty()) {
		cout << "队列是空的!非正常退出!" << endl;
		exit(1);
	}
	else {
		T temp = qList[front];
		count--;
		front = (front + 1) % MAXSIZE;
		
		return temp;
	}

}
template<typename T>
void Queue<T>::QClear(void){
	front = rear =count= 0;
}


template<typename T>
int Queue<T>::QGetCount(void) {
	return count;
}


template<typename T>
bool Queue<T>::QEmpty(){
	return count == 0;
}
template<typename T>
bool Queue<T>::QFull(){
	return count == MAXSIZE;
}

template<typename T>
Queue<T>::~Queue() {

}
//*****************************************************************************************

template<typename T>
BiTNode<T> *CreateBiTTreeFrom(BiTNode<T> *p);

template<typename T>
void DisplayBiTTree(BiTNode<T> *root);

template<typename T>
int GetDepth(BiTNode<T> *root);

template<typename T>
int CountDepth(BiTNode<T> *root);

template<typename T>
void CreateTree(T *arr, int left, int right, BiTNode<T> *p);

template<typename T>
void DeleteTree(BiTNode<T> *root);

int Main() {
	BiTNode<int> *root;
	BiTNode<int> *p = new BiTNode<int>;
	const int size = 9;
	int a[size];
	for (int i = 0;i < size;i++)
		a[i] = i + 10;
	p->data = a[size - 1];

	CreateTree<int>(a, 0, size - 2, p);

	root = CreateBiTTreeFrom<int>(p);
	DisplayBiTTree<int>(root);




	DeleteTree<int>(p);
        DeleteTree<int>(root);
	return 0;
}

//复制二叉树,按BiTNode结构类,添加深度、序号以及父节点等信息
template<typename T>
BiTNode<T> *CreateBiTTreeFrom(BiTNode<T> *p) {
	//复制p为根结点的树,并添加BiTNode相关变量信息。
	Queue<BiTNode<T> *> qbit;
	BiTNode<T> *root=NULL,*bitNode, *childNode;
	if (p) {
		//根结点复制
		root = new BiTNode<T>;
		root->data = p->data;
		root->depth = 1;
		//ordero为节点序号。节点序号编排原则:从0开始;空节点也要占据相应位置。非实际节点序号。
		root->order = 0;
		root->Lchild = p->Lchild;
		root->Rchild = p->Rchild;

		qbit.QInsert(root);
		//其它节点的复制循环,利用队列先进先出特点,按层序复制所有节点。
		while (!qbit.QEmpty()) {
			bitNode = qbit.QDelete();
			//根据父结点,创建左子节点并添加相关信息。
			if (bitNode->Lchild) {
				childNode = new BiTNode<T>;
				childNode->data = bitNode->Lchild->data;
				childNode->depth = bitNode->depth + 1;
				childNode->parent = bitNode;
				//根据父节点的序号确定上一层元素的个数,从而可以确定当前节点在当前层的序号。
				//序号计算要包括父结点中不存在的左或右子节点的数量。序号不是指实际存在的同层节点的序号。
				childNode->order = childNode->parent->order * 2;
				childNode->Lchild = bitNode->Lchild->Lchild;

				childNode->Rchild = bitNode->Lchild->Rchild;

				bitNode->Lchild = childNode;
				qbit.QInsert(bitNode->Lchild);
			}
			//根据父结点,创建右子节点并添加相关信息。
			if (bitNode->Rchild) {

				childNode = new BiTNode<T>;
				childNode->data = bitNode->Rchild->data;
				childNode->depth = bitNode->depth + 1;
				childNode->parent = bitNode;
				childNode->order = childNode->parent->order * 2 + 1;

				childNode->Lchild = bitNode->Rchild->Lchild;

				childNode->Rchild = bitNode->Rchild->Rchild;

				bitNode->Rchild = childNode;
				qbit.QInsert(bitNode->Rchild);
			}

		}

	}
	return root;
}



//经过改造之后的二叉树的图形显示输出
template<typename T>
void DisplayBiTTree(BiTNode<T> *root) {

	//利用队列作为读取二叉树节点的工具
	Queue<BiTNode<T>*> qbit;
	//记录前后两个节点的指针
	BiTNode<T> *p, *pre;
	//需要显示的节点值的宽度。
	//偶数最佳,根据显示调节宽度。不宜太大。
	int deltawidth = 4;
	//整个二叉树的深度(最大层数)
	int treeDepth = GetDepth<T>(root);
	//根据二叉树最大一层可能存在的元素最多个数确定输出的最大宽度
	int maxwidth = deltawidth * pow(2, treeDepth - 1);
	//元素之间间距变量
	//二叉树每层元素以一定宽度输出后,在上述最大宽度输出范围内,剩余的元素之间的空白宽度
		//计算公式:(2^(最大层数-当前层数)-1)/2  ,其中层数从1开始。
	int distance;
	//二叉树深度(层数)
	int depth;
	//同层元素的序号:从0开始,每层元素为:2^(层数-1)个。每层最大序号为:2^(层数-1)-1 个。
	int order;
	//每层可能存在的元素的最大个数。
	int nodesNum;
	//二叉树每层最后一个元素输出后,接着输出换行符后的标志设置
	int flagEnter = 0;
	//上一个元素的序号
	int _preOrder;


	//上一个元素指针(初始为根结点)
	pre = root;
	/*以下利用队列先进先出的特点,循环读取二叉树节点,
	按层序由根结点到最大层从左到右依次读取元素,
	并根据获取的元素相关信息格式化输出二叉树每层元素。
	当遍历二叉树又不想用递归方法时,利用队列的先进先出的特点就能实现遍历了。
		*/
	if (root) {
		qbit.QInsert(root);
		while (!qbit.QEmpty()) {
			//每个节点输出时的输出宽度的变量(用于计算节点输出宽度的过程中的累计)
			int totalwidth = 0;
			p = qbit.QDelete();
			//当前节点的深度(层数)
			depth = p->depth;
			//当前节点的序号(即二叉树某层第几个节点)
			order = p->order;
			//每层元素总数(包括空节点)
			nodesNum = pow(2, depth - 1);
                        //如果上一个是上一行的最后一个节点,输出换行符
                        if (pre->depth != p->depth) {
				cout << endl;
				flagEnter = 1;
			}

			//单位间距变量(关键点!!!)
			//根据二叉树的层数(深度)和自定义设置的元素输出的单位宽度,
			//直接计算出元素之间的间距的单位量:元素之间的间距=2*distance.
			//每层的间距不一样,最大一层间距为0.即distance为0.
			distance = deltawidth * (pow(2, treeDepth - depth) - 1) / 2;
			//根结点所在层,即第一层单独设置输出宽度。
			//第一层:maxwidth=distance+节点(按一定单位的宽度输出)+distance.
			if (depth == 1) {
				cout.width(distance + deltawidth);
			}
			else {
				//以下为:非第一层节点输出的宽度的计算方法
					//如果上一节点是上一层最后一个元素输出并输出了换行符,
					//下一行开始(即当前节点)就要首先输出一个缩进宽度。即为distance的值。
				if (flagEnter == 1) {
					totalwidth += distance;
					flagEnter = 0;
				}

				//存在每层第一个存在的元素的序号不是为0的情况,统一把它第一个元素的序号设置成-1,
					//方便计算元素之间的间距(不一定是序号相连的两个元素)
				if (pre->depth != depth) {
					_preOrder = -1;
				}
				else {
					//不是第一个的元素的序号仍然为原序号。
					_preOrder = pre->order;
				}

				//每个元素的输出宽度的计算公式(元素本身的宽度+元素之间间距)--(实际存在的元素之间不一定序号相连)。
					//同层序号相连的两个元素之间的间距=2*distance.
				totalwidth += (order - _preOrder)*(deltawidth + 2 * distance) - 2 * distance;
				//元素值输出时的宽度设置。
				cout.width(totalwidth);



			}



			//输出元素(节点)的值
			cout << p->data;
			//每个元素输出后的间距输出。最深一层(最大层)的间距为0,不输出。
			if (depth != treeDepth) {
				cout.width(2 * distance);

				cout << " ";
			}
			



			//把当前节点设置成下一个节点的前一个节点。
			pre = p;
			//按层序原则循环输出其它元素。搞定!!!
			if (p->Lchild) {
				qbit.QInsert(p->Lchild);

			}
			if (p->Rchild) {
				qbit.QInsert(p->Rchild);
			}
		}


	}
}

//计算二叉树的深度
template<typename T>
int GetDepth(BiTNode<T> *root) {
	return CountDepth<T>(root);
}


//计算二叉树的深度
template<typename T>
int CountDepth(BiTNode<T> *root) {
	int leftdepth, rightdepth, maxdepth;
	if (root == NULL)
		return 0;
	leftdepth = CountDepth(root->Lchild);
	rightdepth = CountDepth(root->Rchild);
	maxdepth = leftdepth < rightdepth ? rightdepth : leftdepth;
	return maxdepth + 1;

}

//根据数组创建节点只有data,LChild,RChild数据的二叉树
template<typename T>
void CreateTree(T *arr, int left, int right, BiTNode<T> *p) {

	if (p) {

		if (left == right) {
			BiTNode<T> *newnode = new BiTNode<T>;
			newnode->data = arr[left];
			if (!p->Lchild)
				p->Lchild = newnode;
			else if (!p->Rchild)
				p->Rchild = newnode;
		}
		if (left < right) {
			int mid = (left + right) / 2;

			if (p->Lchild)
				CreateTree(arr, left, mid, p->Lchild);
			else
				CreateTree(arr, left, mid, p);

			if (p->Rchild)
				CreateTree(arr, mid + 1, right, p->Rchild);
			else
				CreateTree(arr, mid + 1, right, p);
		}




	}


}

template<typename T>
void DeleteTree(BiTNode<T> *root) {

	if (root != NULL) {

		if (root->Lchild) {
			DeleteTree(root->Lchild);
		}
		if (root->Rchild) {
			DeleteTree(root->Rchild);
		}
		delete root;

	}
}