使用Graphviz打印二叉树
在学习数据结构中的二叉树的时候,我们一般是用二叉链表去表示一棵二叉树。
二叉树有很多操作,例如遍历、线索化、插入结点和删除结点等,很多时候,我们不能直观地看到二叉树的结构,因而可能不确定自己的代码正确与否,所以我们经常需要把二叉树的结构打印出来。
然而由于二叉树是分层的结构,直接在控制台打印并不容易,同时也不美观。于是经过在网上一番搜索,我找到了一款叫Graphviz的工具,这款工具是专门用来绘制各种图用的,具体可以自行上网搜一下。
简单来说,我们需要先生成一个dot文件,然后Graphviz利用这个dot文件给我们生成一张图片。
dot文件长啥样呢?它是一个纯文本文件,内容如下所示:
digraph { label = "# stands for null node" A[shape=circle]; edge [style=solid]; A -> B edge [style=solid]; A -> J B[shape=circle]; edge [style=solid]; B -> C edge [style=solid]; B -> D C[shape=circle]; N0[shape = circle,label = "#",style=dashed]; edge [style=dashed]; C -> N0 N1[shape = circle,label = "#",style=dashed]; edge [style=dashed]; C -> N1 D[shape=circle]; edge [style=solid]; D -> E edge [style=solid]; D -> F E[shape=circle]; edge [style=solid]; E -> G N2[shape = circle,label = "#",style=dashed]; edge [style=dashed]; E -> N2 G[shape=circle]; N3[shape = circle,label = "#",style=dashed]; edge [style=dashed]; G -> N3 edge [style=solid]; G -> H H[shape=circle]; N4[shape = circle,label = "#",style=dashed]; edge [style=dashed]; H -> N4 N5[shape = circle,label = "#",style=dashed]; edge [style=dashed]; H -> N5 F[shape=circle]; N6[shape = circle,label = "#",style=dashed]; edge [style=dashed]; F -> N6 edge [style=solid]; F -> I I[shape=circle]; N7[shape = circle,label = "#",style=dashed]; edge [style=dashed]; I -> N7 N8[shape = circle,label = "#",style=dashed]; edge [style=dashed]; I -> N8 J[shape=circle]; edge [style=solid]; J -> K N9[shape = circle,label = "#",style=dashed]; edge [style=dashed]; J -> N9 K[shape=circle]; N10[shape = circle,label = "#",style=dashed]; edge [style=dashed]; K -> N10 N11[shape = circle,label = "#",style=dashed]; edge [style=dashed]; K -> N11 }
简单来说,其内容描述了一幅图中各个结点信息和结点之间的边的信息。例如:
A[shape=circle]; edge [style=solid]; A -> B edge [style=solid]; A -> J
其中描述了图中存在一个A结点,然后有两条边 A->B 和 A->J 。
当然,dot支持更复杂的格式控制语法,可以让我们设置更多的东西,在这里我们只需要设置实线和虚线。你也可以针对特定的二叉树去进行更加丰富的设置,例如红黑树可以给每个结点设置不同的颜色。
那么Graphviz根据这个dot文件给我们生成的图片长啥样呢?如下所示:
是不是看着形象很多了😉。 至少比你在vs里面调试的时候舒服多了吧。下面是我们在vs里面调试二叉树经常看到的数据
当然,你也可以去github搜一下别人写好的格式控制代码,以让输出的图片更加美观。 比如,我在网上找的这个 GraphViz formatting script for binary trees
利用它,生成的图片长这样:
是不是要更加美观一点了。
那么现在问题来了,我们如何生成这个dot文件呢?
且慢,我们先做一些准备工作。首先我们需要建立一棵二叉树。 简单起见,我们可以根据二叉树的前序遍历序列来递归建立二叉树,先给出二叉树结点的结构定义: `
1 #define ElemType char 2 typedef struct TreeNode 3 { 4 ElemType data; 5 struct TreeNode *lchild, *rchild; 6 TreeNode() : data(0), lchild(nullptr), rchild(nullptr) {} 7 TreeNode(int x) : data(x), lchild(nullptr), rchild(nullptr) {} 8 TreeNode(int x, TreeNode *left, TreeNode *right) : data(x), lchild(left), rchild(right) {} 9 } TreeNode, *TreePtr;
建立二叉树代码
1 void createTree(TreePtr &T, ifstream &fin) 2 { 3 if (!fin.eof()) 4 { 5 ElemType e; 6 fin >> e; 7 if (e == '#') 8 T = nullptr; 9 else 10 { 11 T = new TreeNode(e); 12 createTree(T->lchild, fin); 13 createTree(T->rchild, fin); 14 } 15 } 16 }
我们需要先把二叉树前序遍历序列写入到一个txt文件中,例如,上面那棵二叉树的前序遍历序列为:
ABC##DEG#H###F#I##JK###
注意,空结点#也需要写进去。
然后我们用一个输入流和这个文件关联起来,再使用这个输入流建立二叉树
ifstream fin; fin.open("input.txt"); TreePtr T = nullptr; createTree(T, fin); fin.close()
接下来我们就利用这棵刚刚建好的二叉树去生成我们之前需要的那个dot文件。
我们先不要去管dot文件中的那些格式控制代码,简单起见,我们只需要往其中写入所有的点和所有的边的信息就行。 这件事其实很简单,因为二叉树里面的很多操作都是通过递归完成的,所以这里我们自然也可以通过递归去做 生成dot文件代码
1 void createDot(TreePtr T, ofstream& fout, int& cnt) { 2 if (!T) return; 3 4 //输出当前结点信息 5 fout << T->data << "[shape=circle];\n"; 6 7 //处理左孩子 8 if (T->lchild) { 9 fout << "edge [style=solid];\n"; 10 fout << "\t" << T->data << " -> " << T->lchild->data << "\n"; 11 } 12 else { 13 fout << "\t" << "N" << cnt << "[shape = circle,label = \"#\",style=dashed];\n"; 14 fout << "edge [style=dashed];\n"; 15 fout << "\t" << T->data << " -> " << "N" << cnt++ << "\n"; 16 } 17 18 //处理右孩子 19 if (T->rchild) { 20 fout << "edge [style=solid];\n"; 21 fout << "\t" << T->data << " -> " << T->rchild->data << "\n"; 22 } 23 else { 24 fout << "\t" << "N" << cnt << "[shape = circle,label = \"#\",style=dashed];\n"; 25 fout << "edge [style=dashed];\n"; 26 fout << "\t" << T->data << " -> " << "N" << cnt++ << "\n"; 27 } 28 29 //递归左子树和右子树 30 createDot(T->lchild, fout, cnt); 31 createDot(T->rchild, fout, cnt); 32 }
总体逻辑是:
1. 如果当前是空结点,则直接返回,递归结束。
2. 如果当前结点不空,那么我们需要输出当前结点信息,如果当前结点有左孩子或者右孩子,我们还要输出对应两条边的信息。 然后我们再递归处理左子树和右子树。
其实这个代码跟二叉树的前序递归遍历代码的逻辑是一样的。至于其中输出的很多额外信息,则是dot样式控制代码。 然后我们再进一步封装一下刚才那个函数,于是我们就得到了一个printTree函数,如下:
1 void printTree(TreePtr T, string fileName) { 2 ofstream fout; 3 fout.open(fileName); 4 if (!fout) cout << "open file failed!" << endl; 5 6 fout << "digraph {\nlabel = \"# stands for null node\"\n"; 7 8 int cnt = 0; 9 createDot(T, fout, cnt); 10 fout << "}"; 11 fout.close(); 12 }
其中fileName是我们需要生成的dot文件的文件名。
得到dot文件之后,我们在dot文件所在目录打开控制台,输入以下命令即可生成图片:
dot tree.dot -Tpng -o tree.png
你也可以写好一个脚本,然后在代码里运行这个脚本生成图片。 其中tree.dot是刚才生成的dot文件名,tree.png是生成的图片名
当然,你得事先安装好Graphviz,可以直接从官网安装 Graphviz官网
1 int main() { 2 //创建二叉树 3 ifstream fin; 4 fin.open("input.txt"); 5 if (!fin) cout << "open file failed!" << endl; 6 7 TreePtr T = nullptr; 8 createTree(T, fin); 9 fin.close(); 10 printTree(T, "tree.dot"); 11 }