第四章 VimL 数据结构进阶
4.3 嵌套组合与扩展
VimL 虽然只提供了列表与字典两种数据结构,但通过列表与字典的合理组合,几乎能表 达任意复杂的数据结构。这与许多其他流行的脚本语言(如 python)的思想如出一辙。 本节就讨论在 VimL 中如何用列表与字典表示常用数据结构。
堆栈与队列
堆栈是所谓先进后出的结构,队列是先进先出的结构。这可以直接用一个 list
表示, 因为 list
相当于个动态数组,支持随意在两端增删元素。
如果只在列表尾部增删元素,那就实现了堆栈行为。如果尾部增加而在头部删除,就实现 了队列行为,如:
: function Push(stack, item)
: call add(stack, item)
: endfunction
: function Pop(stack)
: call remove(stack, -1)
: endfunction
: function Shift(queue)
: call remove(stack, 0)
: endfunction
在这个示例中,用 Push/Pop
表示堆栈操作,用 Push/Shift
表示队列操作。这只为 简明地说明算法意图,实际应用中最好先检查 stack/queue
是否为 list
类型,以 及检查列表是否为空。
链表
在脚本语言中,其实根本不用实现链表,因为动态数组本身就可用于需要链表的场合。在 VimL 中,就直接用 list
表示线性链就够了。除非你真的需要很频繁地在一个很长的 list
中部增删元素,那么或可用字典来模拟链表的实现。
例如,以下代码构建了一个有 10 个结点的链表,每个结点是个字典,value
键表示存储 内容,next
表示指向下一个结点:
: let head = {}
: for value in range(10)
: let node = {'value': value, 'next': head}
: let head = node
: endfor
其实在上面的循环中,临时变量 node
可以省略。head
始终指向链表的起始结点, 可通过 next
键依次访问剩余结点,末尾结点的 next
键值是空字典。
这里的关键是,字典的值,或列表元素的值,不仅可以存储像数字与字串符的简单标量, 还可以存储另一个列表或字典(的引用)。基于这样的嵌套与组合,就可以表达更复杂的 数据结构了。
二维数组(矩阵)
如果列表的每个元素都是另一个列表,那就构成了一个二维数组。例如:
: let matrix = []
: for _ in range(10)
: let row = range(10)
: call add(matrix, row)
: endfor
构建了一个 10x10
大小的矩阵,其中每个行向量由 range(10)
生成。这样快速生成 的矩阵每一行都相同,或许不是很有趣,但是可以用以下两层循环重新赋值:
: for i in range(10)
: for j in range(10)
: let matrix[i][j] = i * j
: endfor
: endfor
从数学意义上的矩阵讲,它应是规整的矩形,即每行的长度是一样的。但当在 VimL 中用 列表的列表表示时,其实并不能保证每一行都等长。例如:
: let text = getline(1, '$')
: for i in range(len(text))
: let line = text[i]
: let text[i] = split(line, '\s\+')
: endfor
在这里,首先用 getline()
获取当前 buffer 的所有行,保存在 text
这个列表变 量中,其中每个元素表示一行文本字符串。在随后的循环中,又将每行文本分隔成一个个 单词(空格分隔的字符串),将标量字符串元素转化为了另一个列表。因此,text
最 终结果就是列表的列表,即二维数组。而一般情况下,每行的单词数量是不等,所以这个 二维数组不是规整的矩阵。
事实上,这个示例的循环可以直接用 map()
函数代替:
: let text = getline(1, '$')
: call map(text, "split(v:val, '\\s\\+')")
树
以二叉树为例,也可用一个字典来表示树中某结点,除了需要一个键(如 value
)来保 存业务数据,还用一个 left
键表示左孩子结点,right
表示右孩子结点,这两个应 该都是另一个具有相同结构的字典引用,如果缺失某个孩子,则可用空字典表示。
: let node = {}
: let node.value = 0
: let node.left = {}
: let node.right = {}
这样,只要有一个字典变量引用了这样的一个结点(不妨称之为根结点),就相当于引用 着一棵树,沿着结点的 left
与 right
键就能访问整棵树的所有结点。两个子结点 都是空字典时,该结点就是所谓的叶结点。
不过,由于每个结点含有两个方向的子结点,要遍历树可不是那么直观。有兴趣的读者请 参考相应的树算法。本节内容旨在说明 VimL 的字典用法,展示其表达能力。而算法其实 是与语言无关的。
在上述的树结点字典结构中,只能从一个结点访问其子结点,而无法从子结点访问父结点 。如果有这个需求,只要在每个结点字典中再加一个键引用父结点即可,如:
: let node.parent = {}
每个子结点都有父结点,即 parent
键非空。根结点没有父结点,那 parent
键应该 存个什么值呢?可以就用空字典表示,也可以引用它自身,这都可以将根结点与其他非根 结点区分开来。
我们知道,字典或列表变量都只是某个实体的引用。VimL 的自动垃圾回收机制主要是基 于计数引用的。如果某个字典或列表实体没有被任何变量引用了,即引用计数为 0 时, (在变量离开作用域或显式 :unlet
时会减少引用计数)VimL 就认定该实体无法被访 问了,就会当作垃圾回收其所占用的内存。在大部分简单场合中,这套机制很好用。不过 考虑这里讨论的包含 parent
与 left
right
键的树结点,在父、子结点之间形成 了环引用,它们的引用计数始终不会降到 0 。然而 VimL 另外也有一个算法检测环引用 ,所以也尽可放心使用这个树结构,不必担心内在泄漏。只不过存在环引用时,垃圾回收 的时机可能相对滞后而已。
现在,让我们再考虑一种有任意多个孩子的树(任意叉树)。这种结构在实际应用中是存 在的,比如目录树,每个目录(结点)可以有很多个不确实数量的子目录或文件(叶结点 )。为表示这种结构,我们可以将所有子结点放在一个列表中,然后用一个键引用这个列 表,如下定义每个结点的字典结构:
: let node = {}
: let node.value = 0
: let node.parent = {}
: let node.child = []
与原来的二叉树相比,取消 left
与 right
键,而以统一的 child
键代替。每当 增加一个子结点时,就添加到 child
列表中,同时维护该子结点的 parent
键。如 果 child
键为空列表,就表示该结点为叶结点。
图
图是一些顶点与边的集合,常用 G(V, E)
表示,其中 V
是顶点集合,E
是边集合 ,每条边连接着 V
中两个顶点。一般用 |V|
表示顶点的个数,|E|
表示边数。
用程序表示图,有两种常用的方式,邻接矩阵与邻接表。这里讨论一下如何用 VimL 的数 据结构表示图。
邻接矩阵很简单,就是一个 |V| x |V|
大小的矩阵,假设就用变量名 graph
表示这 个矩阵。前面小节已介绍,矩阵在 VimL 中就是列表的列表。如果顶点 i
与 j
之间 有一条边,就 :let graph[i][j] = 1
,否则就用一个特殊值来表示这两个顶点之间没 有边,比如在很多情况下用 0
表示无边是可行的,:let grapsh[i][j] = 0
。如果是 有权边,则可把边的权重保存在相应的矩阵位置中,如 :let graph[i][j] = w
。如果 是无向图,则再对称赋值 :let graph[j][i] = graph[i][j]
。
由于矩阵元素支持随机访问,用邻接矩阵表示图在某些应用中非常高效简便,尤其中边数 非常稠密的情况下(极限情况是每两个顶点之间都有边的完全连通图)。不过在边数很少 的情况下,这将是个稀疏矩阵,在内存空间使用上比较低效。
邻接表,首先它是包含所有顶点的列表;每个顶点是一个字典结构,它至少有个键 edge
来保存所有与本顶点相关的边,这应是一个边结构的列表;在边结构字典中则保存着权重 weight
,以及它所连接的顶点(字典引用)。大致结构如下所示:
: let graph = [] " a list of vertex
: let vertex = {'edge': [], 'id':0, 'data': {}}
: let edge = {'weight':1, 'target': {}, 'source': {}}
如果只要求自上而下访问边结构,那这个字典中可以只保存一个顶点,另一个顶点就是它 被保存的顶点(由它的 edge
键访问到这个边)。这可以减少一些存储空间,不过顶点 也只是字典引用,保存双端点也浪费不了太多空间。
在实际的图应用中,肯定还会有具体的业务数据,这些数据一般是保存顶点结构中。比如 可以给每个顶点给个 id
编号或名字,如果有大量复杂的数据,可单独保存在另一个字 典引用中。
所以,邻接表虽然复杂,但灵活度高,易扩展业务数据。而邻接矩阵在矩阵元素中只能保 存一个值,扩展有些不方便。除非是业务数据是保存在边结构中,那么在矩阵中可以保存 另一个字典引用,而不是简单的权重数值。
JSON
如果你了解 JSON,就会发现 VimL 的列表与字典的语法表示,正好也是符合 JSON 标准 的。一个有效的 JSON 字符串也是也是合适的 VimL 的表达式,可以直接用于 :let
命 令的赋值。
当然这有一个小小的限制,JSON 字符串不能有换行,因为 VimL 语言是按行解析的,且 续行符比较特殊(在下一行开头使用反斜杠)。如果是不太复杂的 JSON,在 Vim 编辑中 可以将普通命令 J
将多行字符串合并为一行,我不认为你会用其他编辑器写 VimL 脚 本。
此外,有个内置函数 jsondecode()
可将一个合法的 JSON 字符串(允许多行)解析为 VimL 值,以及反函数 jsonencode()
将一个 VimL 表达式转换为 JSON 字符串。
总结
在 VimL 中,用列表与字典的组合,可以表达很复杂很精妙的数组结构,几乎只有想不到 没有做不到。其实这也不必奇怪,因为目前大部分作为高级语言的动态脚本,其思想是相 通的。虽然 VimL 似乎只能用于 Vim,但它与其他流行的外部脚本语言,在某种程序上是 极其相似的。