本教程将教会您如何使用 Algviz 进行算法可视化编程,如果您想了解 Algviz 完整的接口定义,请参考 Algviz API 描述文档。
可视化对象
在使用 Algviz 时,您第一个要创建的对象就是可视化对象 Visualizer。 可视化对象用来同步动画帧、控制每帧动画的时长,以及管理每一个要显示的数据对象。
创建可视化对象
在创建一个可视化对象时,可以为其指定默认的动画帧时长,以及相邻两帧动画的间隔时长。 如果需要调试,可以将 wait
参数设置为 True
,这样 Jupyter Notebook 中的动画就会暂停下来,直到输入继续运行的指令。
import algviz
# 创建可视化对象,默认每帧动画时长 3.0 秒钟,两帧动画间隔 0.5 秒。
viz = algviz.Visualizer(delay=3.0, wait=0.5)
控制动画刷新
当 Visualizer.display 接口被调用时, Algviz 才会在 Notebook 中更新从上一帧到当前时刻所有的数据对象的变化过程,简称为更新动画帧。
viz.display() # 显示第一帧动画,帧时长为默认值 3.0 秒。
# 做一些事情……
viz.display(delay=2.0) # 显示第二帧动画,帧时长为设定值 2.0 秒。
创建数据对象
一个数据对象也是 Algviz 中一个基本的数据容器(例如:向量、树、拓扑图),当您修改数据对象中的内容时, Algviz 会记录它们的变化,并为不同的操作生成相应的动画。而在使用这些数据对象之前,需要通过 Visualizer 中提供的接口来创建它们,下面介绍各个数据对象的创建方式。
向量
向量 Vector 是 Algviz 中定义的一维数组容器。您可以像使用 Python 中的 list
对象一样来使用 Vector 对象,下面是具体的使用方法:
创建向量对象
接口 Visualizer.createVector 用来创建一个向量对象。在创建向量对象时,您可以指定向量中的初始数据、向量对象的名称、单元格的长宽以及是否显示行列下标。如果向量中的元素都是数字,您也可以用 histogram
参考控制是否以直方图的样式来显示向量。下面是一个例子:
vec = viz.createVector(
data = [0, 1, 2, 3], # 向量中的初始化数据;
name = "Vector", # 向量的显示名称;
cell_size = (40, 40), # 向量中基本单元格的显示长宽;
histogram = False, # 是否以条形图的形式显示;
show_index = True # 是否显示下标数字。
)
viz.display()
访问和修改元素
可以通过 for ... in ...
迭代器来按顺序依次访问向量中的元素,也可以通过 []
操作符来访问或是修改向量中指定位置处的元素。
sum = 0
# 通过迭代器依次访问向量中的元素。
for val in vec:
sum += val
vec[0] = sum
viz.display()
vec[0] -= 1
viz.display()
添加元素
- 接口 Vector.append 用来向向量的尾部添加一个新的元素;
- 接口 Vector.insert 用来向向量指定位置处插入一个新的元素。
vec.append(0) # 在向量尾部添加一个新元素 0。
viz.display() # 渲染第一帧动画。
vec.insert(index=4, val=4) # 在索引位置 4 处之前插入元素 4。
viz.display(3) # 渲染第二帧动画。
标记元素
- 接口 Vector.mark 可以将向量中的指定元素标记为指定的颜色。如果将参数
hold
设置为 True,那么标记将不会在后面的动画帧中被移除,否则标记会在下一帧动画中被自动移除; - 接口 Vector.removeMark 用来将指定的颜色标记从向量中移除。
vec.mark(algviz.color_green, 3) # 将向量中位置 3 处的元素标记为绿色,标记将在下一帧被清除。
viz.display(1.0) # 第一帧动画,显示绿色标记。
# 标记向量中 [0, 3) 区间中的所有元素为灰色,标记不会在后面的动画帧中被清除。
vec.mark(algviz.color_gray, st=1, ed=3, hold=True)
viz.display(1.0) # 第二帧动画,显示灰色标记,并清除前一帧动画中不带 hold 属性的标记。
vec.removeMark(algviz.color_gray) # 清除所有的灰色标记。
viz.display(1.0) # 第三帧动画,主动移除灰色标记。
交换元素位置
接口 Vector.swap 可以对向量中指定两个位置处的元素进行交换,并生成对应的动画效果。
vec.swap(0, 5) # 交换向量中位置0和位置4处的元素。
viz.display(3.0)
删除元素
- 接口 Vector.pop 移除向量中指定位置处的元素;
- 接口 Vector.clear 清空向量中的所有元素。
vec.pop(index=2) # 删除索引位置 2 处的元素。
viz.display(3)
vec.clear() # 清除向量中的所有元素。
viz.display(3)
表格
创建表格对象
接口 Visualizer.createTable 用来创建一个新的表格对象。在创建表格对象时,您必须要指定表格有多少行和多少列(后面也可以动态调整表格行列数),可以指定表格中的初始数据、表格对象的名称、单元格的长宽以及是否显示行列下标。下面是一个例子:
tab = viz.createTable(
row = 3, # 表格行数;
col = 3, # 表格列数;
data = None, # 表格中的数据;
name = "Table", # 表格名称;
cell_size = (40, 40), # 单元格的长宽;
show_index = True # 是否显示下标数字。
)
viz.display()
访问和修改元素
可以通过 for ... in ...
迭代器访问表格中的元素,和向量不同的是,您需要两层嵌套的 for ... in ...
才能访问到表格中具体的元素,第一层对表格中的“行”进行迭代,第二层对“行”中的元素进行迭代。此外,也可以通过两次 []
操作来访问表格中的元素,两个 []
中的索引分别对应表格中的某一“行”和某一“列”。
# 通过迭代器访问表格中的所有元素。
sum = 0
for row in tab:
for val in row:
sum += val
# 通过"[]"操作符修改表格中的元素。
tab[1][1] = sum
viz.display(0.5)
标记元素
- 接口 Tabel.mark 可以将表格中的指定元素标记为指定的颜色。和
Vector.mark
类似,如果将参数hold
设置为 True,那么标记将不会在后面的动画帧中被移除,否则标记会在下一帧动画中被自动移除; - 接口 Table.removeMark 用来将指定的颜色标记从表格中移除。
# 标记表格中第一行第一列的元素为绿色。
tab.mark(algviz.color_green, 1, 1)
viz.display(1)
tab.removeMark(algviz.color_green)
viz.display(1)
调整表格形状
- 接口 Table.shape 用来查询表格的当前形状,既行数和列数,返回
(row, column)
; - 接口 Table.reshape 用来修改表格的形状,可以将表格变大或变小。
tab.reshape(row=2, col=2) # 将表格形状改为 2x2。
viz.display(1)
tab.reshape(row=2, col=3) # 将表格形状改为 2x3。
viz.display(1)
在介绍链表、树和拓扑图之前,请您先回顾一下前面介绍的向量和表格。由于向量和表格属于顺序型储存结构,因此可以直接通过向量和表格对象来访问其中的元素,但链表、树和拓扑图属于关系型储存结构,您无法通过某个入口来直接的访问其中的任意个元素。
为了方便的对链表、树和拓扑图中的元素进行操作,您首先需要通过接口 Visualizer.createGraph 创建一个拓扑图可视化对象,该接口的具体使用方法将在后面分别介绍。
链表
创建单向链表
单向链表通过单向链表节点 ForwardLinkedListNode 连接起来,节点中有一个值 val
和指向下一个节点的指针 next
。
Algviz 中提供了 parseForwardLinkedList 接口来创建单向链表,这样就不需要手动的一个个把单向链表节点连接起来了。
head1 = algviz.parseForwardLinkedList([0, 1, 2, 3])
forward_list = viz.createGraph(
data = head1,
name = 'Forward Linked List',
)
viz.display()
创建双向链表
双向链表通过双向链表节点 DoublyLinkedListNode 连接起来,节点中有一个值 val
、一个指向前驱节点的指针 prev
和一个指向后继节点的指针 next
。
同样的,Algviz 中提供了 parseDoublyLinkedList 接口来创建双向链表,该接口返回双向列表的首尾两个节点。
head2, tail2 = algviz.parseDoublyLinkedList([0, 1, 2, 3])
doubly_list = viz.createGraph(
data = head2, # 这里传入链表的头节点就行了。
name = 'Doubly Linked List',
)
viz.display()
修改链表
当链表中有节点的新增或是删除时,Algviz 可以自动检测到它的变化,并生成相应的动画。
# 删除节点 1。
head1_next = head1.next
head1.next = head1_next.next
# 这里将节点从拓扑图中删除。
forward_list.removeNode(head1_next)
viz.display()
# 新增节点 4。
forward_list_node = algviz.ForwardLinkedListNode(4, head1.next.next)
head1.next.next = forward_list_node
viz.display()
提示:上面的代码中用到了 SvgGraph.removeNode 接口,该接口的作用是将一个孤立的节点从拓扑图中删除,被删除的节点将不会在动画中显示。
树
创建二叉树
二叉树通过二叉树节点 BinaryTreeNode 连接起来,二叉树节点中包含了当前节点的值 val
、左子树节点对象 left
和右子树节点对象 right
这三个成员。
您可以手动的去构建二叉树,也可以通过 parseBinaryTree 接口来构造一个二叉树。
# 列表中是二叉树节点值按照层次顺序的排序结果,空的节点使用 None 来表示。
root1 = algviz.parseBinaryTree([1, 2, 3, 4, None, 5, 6])
binary_tree = viz.createGraph(data=root1, name='Binary Tree')
viz.display()
修改二叉树
当树节点或节点之间的关系发生变化时,Algviz 会自动生成对应的动画,例如:
# 交换左右子树。
temp = root1.left
root1.left = root1.right
root1.right = temp
viz.display()
# 节点 2 新增一个子节点。
new_node = algviz.BinaryTreeNode('new')
root1.right.right = new_node
viz.display()
创建多叉树
和二叉树不同,多叉树的每个父节点可能有多个子节点,所以多叉树节点 TreeNode 中包含了一个用于保存子节点的列表,并且支持以下操作来修改其子节点列表:
- TreeNode.children 迭代访问子节点列表中所有的子节点;
- TreeNode.childAt 返回子节点列表中指定位置处的子节点对象;
- TreeNode.childIndex 返回指定节点在子节点列表中的位置索引;
- TreeNode.add 新增一个子节点;
- TreeNode.remove 移除一个子节点;
- TreeNode.removeAt 移除子节点列表指定位置处的子节点。
如果您想创建一个多叉树,可以逐个创建树节点 TreeNode
然后将节点连接起来,也可以使用 parseTree 接口来创建一个多叉树。 下面是一个创建多叉树的例子:
tree_info = {
0: [1, 2, 3], # 节点 0 有三个子节点 1, 2, 3;
1: [4, 5], # 节点 1 有两个子节点 4, 5;
2: [6] # 节点 2 有一个子节点 6。
}
nodes_label = {
0: 'root', # 将节点 0 命名为 root;
3: 'n3' # 将节点 3 命名为 n3。
}
root2 = algviz.parseTree(tree_info, nodes_label)
tree = viz.createGraph(data=root2, name="Tree")
viz.display()
修改多叉树
# 依次迭代所有子节点,并标记当前正在访问的子节点。
for child in root2.children():
tree.markNode(algviz.color_red, child)
viz.display(1.0)
tree_node3 = root2.childAt(2) # 记录节点 3;
tree_node3.add(algviz.TreeNode(8)) # 节点 n3 新增一个子节点 8;
viz.display()
tree_node3.add(algviz.TreeNode(7), 0) # 节点 n3 插入一个子节点 7;
viz.display()
root2.removeAt(1) # 根节点移除子树 2->6;
viz.display()
递归树
递归树是一种特殊的用途多叉树,您可以用递归树来演示递归算法的运行过程,具体的使用方法可以参考博客:递归算法分析工具 RecursiveTree 使用介绍。
拓扑图
创建拓扑图
拓扑图由拓扑图节点 GraphNode 组成,一个拓扑图节点中包含了节点的值和邻居节点的列表等信息。为了方便您使用拓扑图,Algviz 中提供了 parseGraph 接口用于快速创建一个拓补图。
下面展示了一些创建拓扑图的例子:
手动创建拓扑图
# 创建两个拓扑图节点 n1, n2。
n1 = algviz.GraphNode(val = "n1")
n2 = algviz.GraphNode(val = "n2")
# 添加两条边:(n1->n2, e1), (n2->n1, e2)。
n1.add(node=n2, edge="e1")
n2.add(node=n1, edge="e2")
graph1 = viz.createGraph(
data = [n1, n2], # 绑定拓扑图节点;
name = "Graph_1", # 拓扑图名称;
directed = True # 拓扑图为有向图。
)
viz.display()
使用接口创建拓扑图
graph_nodes = algviz.parseGraph(
nodes = [0, 1, 2], # 这里定义节点的 ID(不能有重复值);
edges = [
(0, 1, 'a-b'), # 节点 0 指向节点 1 的边,显示为 a-b;
(1, 2, 'b-c'), # 节点 1 指向节点 2 的边,显示为 b-c;
(2, 0, 'c-a') # 节点 2 指向节点 0 的边,显示为 c-a;
],
nodes_label = {
0: 'a', # 节点 0 的显示名称为 a;
1: 'b', # 节点 1 的显示名称为 b;
2: 'c' # 节点 2 的显示名称为 c;
},
directed = True # 拓扑图为有向图;
)
graph2 = viz.createGraph(graph_nodes, "Graph_2", True)
viz.display()
修改拓扑图
当修改拓扑图节点的邻居信息时,Algviz 会自动调整拓补图的布局,并生成相应的动画,具体的操作接口为:
- GraphNode.neighbors 迭代所有的邻居节点;
- GraphNode.neighborAt 返回邻居列表中索引位置处的节点对象;
- GraphNode.neighborIndex 返回给定节点在邻居列表中的索引位置;
- GraphNode.add 新增或插入一个邻居节点;
- GraphNode.remove 移除一个邻居节点;
- GraphNode.removeAt 移除邻居节点中指定索引位置处的节点。
下面看一些具体的例子:
graph_nodes[2].remove(graph_nodes[0]) # 移除从节点 c 指向节点 a 的边;
viz.display()
graph_node_d = algviz.GraphNode('d') # 新建节点 d;
graph_nodes[1].add(graph_node_d, 'b-d', 0) # 添加一条从节点 b 指向节点 d 的边;
graph_node_d.add(graph_nodes[0], 'd-a') # 添加一条从节点 d 指向节点 a 的边。
viz.display()
光标
光标对象 Cursor 可以用来指示向量或表格中的索引位置。当您通过光标来访问向量或表格中的对象时,Algviz 会自动将光标和对应的向量或表格对象进行绑定,并且在其对应位置处的元素旁添加光标移动的动画。
创建光标对象
要使用光标,首先需要创建一个光标对象。接口 Visualizer.createCursor 用来创建一个光标对象,您在创建时可以指定光标的初始位置和名称。 (注意,光标的位置只能是整数,其他类型的值是没有意义的。)
i = viz.createCursor(
offset = 1, # 光标的初始索引位置为 1;
name = "i" # 光标的名称为 i。
)
j = viz.createCursor(3, "j")
光标被创建之后,默认情况下是不会出现在动画中的,只有当您使用光标来访问向量或是表格中的元素时, 才会在相关向量或表格对象中显示光标的位置。
此外,Algviz 还提供了 Visualizer.cursorRange 接口用于批量创建光标对象,这在迭代访问元素时十分有用。
更新光标位置
有了光标对象以后,您就可以把它当作一个整数来使用, 而且可以通过光标来访问向量和表格中的元素。
光标对象支持常见的算术运算操作: +
、-
、*
、//
、%
、+=
、-=
、*=
、//=
和%=
, 同时也支持常用的比较操作:>
、<
、>=
、<=
、==
和!=
。
如果想要更新光标的位置到任意一个整数,那么需要使用 <<
操作符。 下面通过例子来演示如何使用光标对象:
访问向量中元素
直接将光标对象作为参数传给向量的 []
操作符,即可访问向量中的元素。
提示:一个向量或表格对象可以绑定多个光标对象!
# 创建一个新的向量对象。
vec2 = viz.createVector([0, 1, 2, 3], "Vector2", show_index=True)
# 通过光标 i 依次访问向量中的元素。
while i < len(vec2):
vec2[j] = vec2[i] + 1
viz.display(1.0)
i += 1
j -= 1
viz.display(1.0)
访问表格中元素
i << 0 # 将光标 i 的位置设置为 0;
(r, c) = tab.shape() # 使用前面创建的表格对象;
while i < r: # 迭代访问表格中的每一行;
while j < c: # 迭代访问表格中的每一列;
tab[i][j] = 0 # 更新当前访问的元素。
viz.display(1.0)
j += 1 # 光标 j 移动到下一列;
i += 1 # 光标 i 移动到下一行;
j << 0 # 光标 j 移动到第一列。
viz.display()
其他
打印日志
Algviz 中提供了日志 Logger 对象,您可以把它看作一块滚动刷新的屏幕。
要打印日志,首先您需要创建一个日志对象 Visualizer.createLogger 并在初始化时设置好日志的最大行数,之后调用 Logger.write 接口向日志中写入要显示的信息。如果当前显示的日志行数超出给定的限制时,最早显示的日志会被自动移除掉。
此外,您也可以通过调用接口 clear 随时清空当前显示的日志。下面是一个使用例子:
log = viz.createLogger(3)
log.write('line1')
log.write('line2')
viz.display(1.0)
log.write('line3')
viz.display(1.0)
log.write('line4')
viz.display(1.0)
log.clear()
log.write('line5')
viz.display(1.0)
颜色对照表
在调用 Vector.mark、 Table.mark 以及 SvgGraph.markNode 等接口时,您需要传入一个表示 RGB 颜色值的三元组。为了方便使用,Algviz 中提供了一些预定义的颜色值(如:algviz.color_red
),下面是颜色对照表: