前面已经初步认识了table,这次对table进行深入学习。
table类型实现了“关联数组(associative)”,它是一种具有特殊索引方式的数组,
不仅可以用整数来索引它,还可以使用字符串或其他类型的值(除了nil)来索引它。
此外,table没有固定的大小,可以动态地添加任意数量的元素到一个table中。
table是Lua中主要的(事实上也是仅有的)数据结构机制,具有强大的功能。
基于table,可以以一种简单、统一和高效的方式来表示普通数组、符号表(symbol table)、集合、记录、队列和其他数据结构。Lua也是通过table来表示模块(module)、包(package)和对象(object)的。当输入io.read的时候,
其含义是 ” io模块中的read函数 “。对Lua而言,这表示“ 使用字符串“ read "
作为key来索引 table io”。
1、table的构造式(constructor expression)
tab1 = {78, 45,8}
tab2 = {x = 78, y = 45,z = 99}
tab1是用构造式初始化一个数组,tab2使用了Lua提供的一种特殊的语法用于
初始化记录风格的table,也可以将两种风格混合使用。
注意:
local a = {}
x = "y"
a[x] = 10
print("a[x] = ", a[x]) --->10
print("a.x = ", a.x) --->nil
print("a.y = ", a.y) --->10
上面的代码中,a.x 和 a[x] 是有区别的:a.x 相当于 a["x"]
2、table的内存与长度操作
table永远是“匿名的(anonymous)”,一个持有table的变量与table自身之间
没有固定的关联性。当一个程序再也没有一个table的引用时,Lua的垃圾回收器(garbage collector)最终会删除该table,并复用它的内存。
a = {}
a["x"] = 10
b = a --->a与b引用了同一个table
print(b["x"]) --->10
b["x"] = 20
print(a["x"]) --->10
a = nil --->还有b在引用table
b = nil -->再也没有对table的引用了,gc会删除该table
和string的长度操作一样,用一个井号(#)就能得到table的长度:
-- 打印表 a 的所有元素
for i = 1, #a do
print(a[i])
end
print(a[#a]) --->打印a中的最后一个值
a[#a] = nil--->删除a中的最后一个值</span>
a[#a + 1] = v --->在尾部添加一个值</span>
注意:
1). 再次提醒Lua中的下标是从1开始的,但是你也可以让元素的下标从任意值开始。
2).Lua 5.0不支持长度操作符,可以使用函数table.getn来获得类似的结果。建议使用最新的Lua,
现在更新到 5.2 了。
3).Lua 将nil作为界定数组结尾的标志。所以,当数组中有空隙时(Hole,即有nil),长度操作符
会认为这些nil 元素就是结尾标记。
4). Lua 5.1 新加的一个函数table.maxn 可以返回一个table的最大正索引。
3、table实现的数据结构
1).数组,前面已经接触到数组了:
-- 新建一个数组
a = {}
for i = 1, 1000 do
a[i] = 0
end
也可以用多维数组来表示一个矩阵:
-- 初始化一个 N * M 的矩阵
matrix = {}
for i = 1, N do
matrix[i] = {}
for j = 1, M do
matrix[i][j] = 0
end
end
2).链表:链表中每个节点具有两个字段:指向下一个节点的next和当前节点
保存的值value。
head = nil--->表头
head = { next = head, value = 90} --->在表头插入一个元素
list1 = { next = list1, value = 91}
head.next = list1
list2 = { next = list2, value = 92}
list1.next = list2
local x = head
while x do
print(x.value)
x = x.next
end
3). 队列与双向队列:实现队列的一种简单方法是使用table库的函数 insert 和 remove。
这两个函数可以在一个数组的任意位置插入或删除元素,并且根据操作要求移动后续元素。
一种更高效的实现是使用两个索引,分别用于首尾的两个元素:
queue = {}
function queue.new( )
return { first = 0, last = -1}
end
function queue.pushFront(q, v)
-- body
local first = q.first - 1
q.first = first
q[first] = v
end
function queue.pushBack(q, v)
-- body
local last = q.last + 1
q.last = last
q[last] = v
end
function queue.popFirst(q)
-- body
local first = q.first
if first > q.last then
error("queue is empty")
end
local value = q[first]
q[first] = nil
q.first = first + 1
return value
end
function queue.popLast(q)
-- body
local last = q.last
if q.first > last then
error("queue is empty")
end
local value = q[last]
q[last] = nil
q.last = last - 1
return value
end
-- 打印队列元素
function queue.travel(q)
-- body
local first = q.first
if first > q.last then
error("queue is empty")
end
while first < q.last do
local value = q[first]
print(value)
first = first + 1
end
end
4).
图:图的实现比较复杂,这里参考《Programming in Lua》中的代码。
每个节点表示为一个table,这个table有两个字段:name和adj
(与此节点邻接的节点集合)。
-- 给定节点名称返回对应的节点
local function name2node (graph, name)
if not graph[name] then
graph[name] = { name = name, adj = {} }
end
return graph[name]
end
-- 从文件中读取图数据
function readgraph ()
local = graph = { }
for line in io.lines() do
local namefrom, nameto = string.match(line, "(%S+(%S+)")
local from = name2node(graph, namefrom)
local to = name2node(graph, nameto)
from.adj[to] = true
end
return graph
end
-- 使用深度优先遍历
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
3、table内部的原理:元表(metatable)与元方法(metamethod)
元表与元方法是很重要的概念,后面会查找相关资料,做更详细的了解,
这里只是针对table中的元方法做简单介绍。
前面已经接触过,当访问table中不存在的key时就会返回一个nil,这是正常状况。
其实,table内部的访问机制是:当访问一个table中不存在的字段时,会促使
解释器去查找一个叫__index的元方法。如果没有这个元方法,那么访问的
结果就是nil,否则就由这个元方法来提供最终结果。对于添加元素,
也有一个类似的__newindex元方法。而这些方法是元表中提供的,所以
需要设置一个table的元表,用setmetatable函数即可。下面给出一个
带有默认值的table示例:
function setDefault (t, d)
local mt = { __index = function () return d end }
setmetatable(t, mt)
end
tab = { x = 10, y = 12 }
print( tab.x, tab.z ) ----->10 nil
setDefault( tab, 0 )
print( tab.x, tab.z ) ---->10 0
3、table库:由一些辅助函数构成,这些函数将table作为数组来操作。
1). 插入与删除:
table.insert ( t, pos, value ) -->将元素(value)插入到一个数组(t)的指定位置(pos),
如果没有指定位置参数,则默认将元素添加到数组的末尾。一般,用 t[#t + 1] 来
给一个列表添加元素。
table.remove( t ,pos ) 会删除(并返回)数组指定位置上的元素,并将该位置
之后的所有元素前移,以填补空隙。如果不指定位置参数,默认会删除数组的
最后一个元素。对于数据量比较小时,用这些方法比较合适,数据量大时开销
就太高,需要用更高级的算法来实现相关操作。
2). 排序:table.sort( t, cmp ), 可以对一个数组进行排序,还可以指定一个可选
的次序函数cmp。此参数是一个外部函数, 可以用来自定义sort函数的排序标准.
此函数应满足以下条件: 接受两个参数(依次为a, b), 并返回一个布尔型的值,
当a应该排在b前面时, 返回true, 反之返回false.
例如, 当我们需要降序排序时, 可以这样写:
> sortFunc = function(a, b) return b < a end
> table.sort(tbl, sortFunc)
> print(table.concat(tbl, ", "))
------>gamma, delta, beta, alpha
用类似的原理还可以写出更加复杂的排序函数. 例如, 有一个table存有工会
三名成员的姓名及等级信息:
guild = {}
table.insert(guild, {
name = "Cladhaire",
class = "Rogue",
level = 70,
})
table.insert(guild, {
name = "Sagart",
class = "Priest",
level = 70,
})
table.insert(guild, {
name = "Mallaithe",
class = "Warlock",
level = 40,
})
对这个table进行排序时, 应用以下的规则: 按等级升序排序, 在等级相同时,
按姓名升序排序.
可以写出这样的排序函数:
function sortLevelNameAsc(a, b)
if a.level == b.level then
return a.name < b.name
else
return a.level < b.level
end
end
3). 连接:table.concat(table, sep, start, end)
concat是concatenate(连锁, 连接)的缩写. table.concat()函数列出参数中
指定table的数组部分从start位置到end位置的所有元素, 元素间以指定的
分隔符(sep)隔开。除了table外, 其他的参数都不是必须的, 分隔符的
默认值是空字符, start的默认值是1, end的默认值是数组部分的总长.
sep, start, end这三个参数是顺序读入的, 所以虽然它们都不是必须参数,
但如果要指定靠后的参数, 必须同时指定前面的参数.
> tbl = {"alpha", "beta", "gamma"}
> print(table.concat(tbl, ":"))
alpha:beta:gamma
> print(table.concat(tbl, nil, 1, 2))
alphabeta
> print(table.concat(tbl, "\n", 2, 3))
beta
gamma