Graphviz: 可视化调试利器
riteme.site

Graphviz: 可视化调试利器

当你的各种树出现奇奇怪怪的问题时,你是如何找到错误的呢?
printf?GDB?肉眼?
当然这些方法当然可行,然而把它画出来岂不更秒?
现在祭出利器:Graphviz

简介

Graphviz is open source graph visualization software. Graph visualization is a way of representing structural information as diagrams of abstract graphs and networks. It has important applications in networking, bioinformatics, software engineering, database and web design, machine learning, and in visual interfaces for other technical domains.
gv

翻译:

Graphviz是一个开源的图形可视化软件。图形可视化是表示诸如图表的结构化的抽象图形或网络。在网络技术、生物信息学、软件工程、数据库、网页设计、机器学习和可视化界面及其他可以领域大有用处。

详情参见Graphviz官网

安装

这个不是本文的重点。我只知道对于Debian/Ubuntu用户可以按照以下方式安装:

1
sudo apt-get install graphviz

使用

Graphviz使用一种领域特定语言(Domain-Specific Language1)来描述一副图。这里的图就是指图论中的图。然后Graphviz通过自动布局器来绘制出这个图。由于布局不是由我们手动决定的,因此生成的图的质量依赖于布局算法。

大多数情况下,Graphviz的默认布局器dot可以胜任布局这一任务。下面我们将使用dot

Hello, GRAPHVIZ!

按照惯例,总得有个Hello, world!
当然不要着急,我们先创建一个dot脚本文件:

1
touch hello-world.dot

用你喜欢的编辑器来编辑它:

1
vim hello-world.dot

写入以下内容,保存:

1
2
3
4
5
digraph {
    a -> b;
    b -> c;
    a -> c;
}

生成SVG图片:

1
dot hello-world.dot -Tsvg > hello-world.svg

用你喜欢的图片查看器来看看效果:

1
eog hello-world.svg

如果一路上不出意外,你可以看到下面的结果:
Hello, world!

恭喜你成功创建了一张有向图。

图的类型

在上面,我们创建了一张带有三个顶点的有向图。然而我们有时候不一定要的就是有向图。
如果需要无向图,将digraph换为graph,并把有向边换为无向边即可:

1
2
3
4
5
graph {  // <-- Here
    a -- b;
    b -- c;
    a -- c;
}

此时的图是这样的:
Hello, world!

当然我们可以添加坑爹的平行边和自环:

1
2
3
4
5
6
7
graph {
    a -- b;
    b -- c;
    a -- c;
    c -- c;  // 自环 x 1
    a -- c;  // 平行边 x 1
}

于是乎图长这样:
Hello, world!

当然,如果你不想要它们出现,你可以利用strict将这张图变为严格的图:

1
2
3
4
5
6
7
strict graph {  // <-- strict HERE
    a -- b;
    b -- c;
    a -- c;
    c -- c;
    a -- c;
}

此时平行边已经不见了,毕竟它们的含义是一样的。但是自环还是会留下来的:
Hello, world!

顶点

个人感觉椭圆实在太难看,用来调试完全体现不出B格。
把它换成圆形就好看多了:

1
2
3
4
5
6
7
8
9
graph {
    node [shape = circle];  // 所有顶点全部变成圆形

    a -- b;
    b -- c;
    a -- c;
    c -- c;
    a -- c;
}

就变成这样:
Hello, world!

啊!我想让c变成正方形!

当然也没有问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
graph {
    node [shape = circle];  // 所有顶点全部变成圆形
    c [shape = square];  // 指定c变为正方形

    a -- b;
    b -- c;
    a -- c;
    c -- c;
    a -- c;
}

Hello, world!

不行我要三角形!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
graph {
    node [shape = circle];  // 所有顶点全部变成圆形
    c [shape = triangle];  // 指定c变为三角形

    a -- b;
    b -- c;
    a -- c;
    c -- c;
    a -- c;
}

Hello, world!

好吧如果你还要其它的图形,可以参见http://www.graphviz.org/content/node-shapes

光有形状有卵用,加点颜色才好玩:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
graph {
    node [shape = circle];
    a [color = blue];  // 变蓝色
    b [color = red]; // 变红色
    c [fontcolor = green];  // 字体颜色变绿

    a -- b;
    b -- c;
    a -- c;
    c -- c;
    a -- c;
}

Hello, world!

更详细的颜色名称表在此:http://www.graphviz.org/content/color-names

现在我们来画一棵二叉搜索树:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
digraph {
    node [shape = circle];

    3 -> 2;
    3 -> 8;
    2 -> 1;
    8 -> 5;
    8 -> 9;
    5 -> 4;
    5 -> 7;
    7 -> 6;
}

得到的结果是这样的:
Hello, world!

似乎并不尽人意,难以分辨左儿子和右儿子。

因此我们可以通过顶点的方向来确定。
每个顶点有八个方向:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
digraph {
    node [shape = circle];

    x :e -> e;
    x :ne -> ne;
    x :n-> n;
    x :nw -> nw;
    x :w -> w;
    x :sw -> sw;
    x :s -> s;
    x :se -> se;
}

下面的图片展示了顶点的八个方向2(这张图是用circo生成的,命令行参数一样):
Hello, world!

这些方向和东南西北的表示是一样的。
刚才我们指定的是出发的方向,当然我们也可以指定进入的方向。

因此我们来将左右儿子的边的出发方向修改一下,左孩子出发方向为:sw,右孩子的出发方向为:se

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
digraph {
    node [shape = circle];

    3:sw -> 2;
    3:se -> 8;
    2:sw -> 1;
    8:sw -> 5;
    8:se -> 9;
    5:sw -> 4;
    5:se -> 7;
    7:sw -> 6;
}

Hello, world!
虽然不是那么规整,但是足以分辨出左右儿子了。

对于有些树,我们会记录父亲节点,因此我们加一条指向父亲的边:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
digraph {
    node [shape = circle];

    3:sw -> 2;
    2 -> 3;
    3:se -> 8;
    8 -> 3;
    2:sw -> 1;
    1 -> 2;
    8:sw -> 5;
    5 -> 8;
    8:se -> 9;
    9 -> 8;
    5:sw -> 4;
    4 -> 5;
    5:se -> 7;
    7 -> 5;
    7:sw -> 6;
    6 -> 7;
}

得到的效果是这样的:
Hello, world!

话说我分不清哪个是指向父亲的链接了!!!

呃…没关系,我们把左右儿子的链接加粗就分得清了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
digraph {
    node [shape = circle];

    3:sw -> 2 [style = bold];
    2 -> 3;
    3:se -> 8 [style = bold];
    8 -> 3;
    2:sw -> 1 [style = bold];
    1 -> 2;
    8:sw -> 5 [style = bold];
    5 -> 8;
    8:se -> 9 [style = bold];
    9 -> 8;
    5:sw -> 4 [style = bold];
    4 -> 5;
    5:se -> 7 [style = bold];
    7 -> 5;
    7:sw -> 6 [style = bold];
    6 -> 7;
}

Hello, world!
上面是对边进行设置,将边加粗。

指向区别能更明显些吗?

干脆把它做成虚的吧:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
digraph {
    node [shape = circle];

    3:sw -> 2 [style = bold];
    2 -> 3 [style = dotted];
    3:se -> 8 [style = bold];
    8 -> 3 [style = dotted];
    2:sw -> 1 [style = bold];
    1 -> 2 [style = dotted];
    8:sw -> 5 [style = bold];
    5 -> 8 [style = dotted];
    8:se -> 9 [style = bold];
    9 -> 8 [style = dotted];
    5:sw -> 4 [style = bold];
    4 -> 5 [style = dotted];
    5:se -> 7 [style = bold];
    7 -> 5 [style = dotted];
    7:sw -> 6 [style = bold];
    6 -> 7 [style = dotted];
}

Hello, world!

像某些数据结构,可能会有Lazy标记之类的,我们可能需要对节点作特殊标记来标明。
这当然也是可以实现的,只需提前声明好即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
digraph {
    node [shape = circle];

    3 [color = red];
    5 [color = red];

    3:sw -> 2 [style = bold];
    2 -> 3 [style = dotted];
    3:se -> 8 [style = bold];
    8 -> 3 [style = dotted];
    2:sw -> 1 [style = bold];
    1 -> 2 [style = dotted];
    8:sw -> 5 [style = bold];
    5 -> 8 [style = dotted];
    8:se -> 9 [style = bold];
    9 -> 8 [style = dotted];
    5:sw -> 4 [style = bold];
    4 -> 5 [style = dotted];
    5:se -> 7 [style = bold];
    7 -> 5 [style = dotted];
    7:sw -> 6 [style = bold];
    6 -> 7 [style = dotted];
}

Hello, world!

用于调试

经过上面的简单介绍,Graphviz已经可以用于调试了。
dot脚本非常的便于程序生成,因此我们可以在程序运行中途生成脚本,然后使用dot将其处理后并展示出来。
处理的代码大致是这个样子:

1
2
3
4
5
6
7
8
function SHOW(x):
    buffer = "graph {"

    // 读取数据并生成dot脚本

    buffer += "}"
    write buffer to a dot file
    SYSTEM "dot a.dot -Tsvg > a.svg && eog a.svg"

小结

Graphviz非常强大,这里只是介绍了一小部分功能,如想深入学习可以参见Graphviz官方文档或者Graphviz中文教程指南

利用Graphviz我们可以将图论算法、数据结构的调试过程可视化,从而更加方便我们找到错误所在。

当然,可以利用Graphviz进行对算法过程的截图,从而生成整个算法的流程。

如果你想利用Graphviz来制作插图,那么自动布局就可能不能使你满意。此时个人推荐ProcessOn或者其它的流程图制作软件来绘制,效果更好。


  1. 维基页面https://en.wikipedia.org/wiki/Domain-specific_language 

  2. 事实上,Graphviz中可以定义更加更加精确的方向。但大多数情况下,这几个方向足矣。