Lua中的table不是一种简单的数据结构,它可以作为其他数据结构的基础。其他语言提供的数据结构,如数组、记录、线性表、队列、集合等,在Lua中都可以通过table来表示。
数组
使用整数来索引table即可在Lua中实现数组。 因此,数组没有一个固定的大小,可以根据需要增长。
长度操作符 #计算数组的大小
可以使用0、1或其他任意值来作为数组的起始索引。
在Lua中习惯一般以1作为数组的起始索引。Lua库和长度操作符都遵循这个约定。如果你的数组不是从1开始的,就无法使用这些功能。
矩阵与多维数组
在Lua中有两种方式来表示矩阵。一种是使用一个“数组的数组”:
mt = {} --创建矩阵
for i = 1, N do
mt[i] = {} --创建一个新行
for j = 1, M do
mt[i][j] = 0
end
end
由于在Lua中table 是一种对象,因此在创建矩阵时,必须显示地创建每一行。
另一种方式是将两个索引合并为一个索引。如果两个索引是整数,可以将第一个索引乘以一个适当的常量,并加上第二个索引。创建N*M的零矩阵:
mt = {} --创建矩阵
for i = 1, N do
for j = 1, M do
mt[(i - 1) * M + j] = 0
end
end
如果索引是字符串,那么可以把索引 拼接起来,中间使用一个字符来分割。例如,使用字符串s和t来索引一个矩阵,可以通过代码m[s .. ":" .. t]。其中,s和t都不能包含冒号,否则像("a:", "b")或("a", ":b")这样的索引会是最终索引变成"a::b"。如果无法保证这点的话,可以使用例如'\0'这样的控制字符来分割两个索引。
对于“稀疏矩阵”,Lua中的数组都是以table表示的,它们本身就是稀疏的,所以很容易节省内存。
链表
每个结点以一个table来表示,一个“链接”只是结点table中的一个字段,该字段包含了对其他table的引用。
列表头:
list = nil
在表头插入一个元素,元素值为v:
list = {next = list, value = v}
遍历此列表:
local l = list
while l do
<访问l.value>
l = l.next
end
队列与双向队列
在Lua中实现队列的一种简单的方式是使用table库的函数insert和remove。这两个函数可以在一个数组的任意位置插入或删除元素,并且根据操作要求移动后续元素。不过对于较大的结构,移动的开销是很大的。一种更高效的实现是使用两个索引,分别用于首尾的两个元素:
function ListNew ()
return {first = 0, last = -1}
end
为了避免污染全局名称空间,将在一个table内部定义所有的队列操作,这个table且称为List。
List = {}
function List.new ()
return {first = 0, last = -1}
end
可以在常量时间内完成在两端插入或删除元素:
function List.pushfirst (list, value)
local first = list.first - 1
list.first = first
list[first] = value
end
function List.pushlast (list, value)
local last = list.last + 1
list.last = last
list[last] = value
end
function list.popfirst (list)
local first = list.first
if first > list.last then error("list is empty") end
local value = list[first]
list[first] = nil --为了允许垃圾收集
list.first = first + 1
return value
end
function List.poplast (list)
local last = list.last
if first > list.last then error("list is empty") end
local value = list[last]
list[last] = nil --为了允许垃圾收集
list.last = last - 1
return value
end
集合与 无序组
在Lua中有一种高效且简单的方式来表示这类集合,就是将集合元素作为索引放入一个table中。那么对于任意值都无需搜索table,只需要用该值来索引table,并查看结果是否为nil。比如Lua中保留字集合:
reserved = {
["while"] = true, ["end"] = true, ["function"] = true, ["local"] = true,
}
包,有时也称为“多重集合”,其每个元素都可以出现多次。在Lua中,包的表示类似于上面的集合表示,只不过包需要将一个计数器与table的key关联。若要插入一个元素,则需要递增其计数器:
function insert (bag, element)
bag[element] = (bag[element] or 0) + 1
end
function remove (bag, element)
local count = bag[element]
bag[element] = (count and count > 1) and count - 1 or nil
end
只有当计数器已存在或大于0时,才保留它。
字符串缓冲
local buff = ""
for line in io.lines() do
buff = buff .. line .. "\n"
end
在每次循环中,Lua都会将当前buff中的内容拷贝到一个新字符串中,然后做与line的拼接操作。非常消耗性能。这个问题不是Lua所特有的,在其他语言中,只要字符串是不可变的值,也会有类似的问题。Java就是最有名的例证。
对于较小的字符串,上述循环可以很好地工作。当需要读取整个文件时,Lua提供了io.read("*all")选项,可以一次性读取整个文件。在Lua中,我们可以将一个table作为字符串缓冲。其关键是使用函数table.concat,它会将给定列表中的所有字符串连接起来,并返回连接结果。
local t = {}
for line in io.lines() do
t[#t + 1] = line .. "\n"
end
local s = table.concat(t)
concat和io.read("*all")都使用了一个相同的算法来连接许多小的字符串。
算法核心:一个栈,已创建的大字符串位于栈底,而较小的字符串则通过栈顶进入。对栈中元素的处理类似于著名的“汉诺塔”。栈中的任意字符串都比下面的字符串短。如果压入的新字符串比下面的已存在的字符串长,就将两者连接。然后,再将连接后的新字符串与更下面的字符串比较,依次循环,直到遇到一个更大的字符串或者达到了栈底为止。
function addString (stack, s)
stack[#stack + 1] = s
for i = #stack - 1, 1, -1 do
if #stack[i] > #stack[i + 1] then
break
end
stack[i] = stack[i] .. stack[i + 1]
stack[i + 1] = nil
end
end
为了获取栈缓冲中的最终内容,只需要连接其中所有的字符串就可以了。
图
Lua允许程序员写出多种图的实现,每种实现都有其所使用的算法。接下来,将介绍一种简单的面向对象的实现方式,其中结点表示为对象及边表示为结点间的引用。
每个结点表示为一个table,这个table有两个字段:name和adj(与此结点邻接的结点集合)。由于从文本文件中读取图数据,所以需要一种通过一个结点名来找到该结点的方法。因此,使用了一个额外的table来将名称对应到结点。
local function name2node (graph, name)
if not graph[name] then
--结点不存在,创建一个新的
graph[name] = {name = name, adj = {}}
end
return graph[name]
end
readgraph()函数用于构造一个图。它逐行地读取一个文件,文件中的每行都有两个结点名称,表示了在两个结点之间有一条边,边的方向从 第一个结点到第二个结点。函数对于每行都使用string.match来切分一行中的两个名称,然后根据名称查找结点,最后连接结点。
function readgraph ()
local graph = {}
for line in io.lines() do
--切分行中的两个名称
local namefrom, nameto = string.match(line, "(%S+)%s+(%S+)")
--查找相应的结点
local from = name2node(graph, namefrom)
local to = name2node(graph, nameto)
--将‘to’添加到‘from’的邻接集合
from.adj [to] = true
end
return graph
end
基于上述函数的图的使用算法:
函数findpath采用深度优先的遍历,在两个结点间搜索一条路径。curr-当前结点,to-目标结点,path-保存从起点到当前结点的路径,visited-所有已访问过结点的集合(用于避免回路)。该算法直接对结点进行操作,而不是操作结点名称。
function findpath (curr, to, path, visited)
path = path or {}
visited = visited or {}
if visited[curr] then --结点是否已经访问过?
return nil
end
visited[curr] = true --将结点标记为已访问过
path[#path + 1] = curr --将其加入到路径中
if curr == to then
return path --最后的目标结点
end
--尝试所有的邻接结点
for node in pairs(curr.adj) do
local p = findpath(node, to, path, visited)
if p then return p end
end
path[#path] = nil --从路径中删除结点
end
测试:
function printpath (path)
for i = 1, #path do
print(path[i].name)
end
end
g = readgraph()
a = name2node(g, "a")
e = name2node(g, "e")
print "=========all node========"
for i, v in pairs(g) do
print(i)
end
p = findpath(a, e)
print("=======" .. a.name .. " to " .. e.name .. " :")
if p then printpath(p) end
输入输出测试:
a b
a c
a d
b d
b e
c e
e f
=========all node========
a
c
b
e
d
f
=======a to e :
a
b
e