第 10 章 数据结构
table 是 Lua中唯一的数据结构,其它语言所提供的其它数据结构比方:arrays、 records、lists、queues、sets 等,Lua 都是通过 table 来实现,而且在 lua 中 table 非常好的实 现了这些数据结构。
在传统的 C 语言或者 Pascal 语言中我们常常使用 arrays和 lists(record+pointer)来 实现大部分的数据结构。在 Lua中不仅能够用 table 完毕相同的功能。并且 table 的功能 更加强大。
通过使用 table 非常多算法的实现都简化了。比方你在 lua 中非常少须要自己去实 现一个搜索算法,由于 table 本身就提供了这种功能。
我们须要花一些时间去学习怎样有效的使用table,以下我们通过一些样例来看看假设通过 table 来实现一些经常使用的数据结构。首先。我们从 arrays 和 lists 開始,不仅由于它是其它数据结构的基础。并且是我们所熟悉的。在第一部分语言的介绍中。我们己经接 触到了一些相关的内容。在这一章我们将再来完整的学习他。
10.1 数组
在 lua 中通过整数下标訪问表中的元素就可以简单的实现数组。而且数组不必事先指 定大小,大小能够随须要动态的增长。通常我们初始化数组的时候就间接的定义了数组的大小,比方以下的代码:
a = {} --new array for i=1, 1000 do a[i] = 0 end
通过初始化。数组 a 的大小己经确定为 1000,企图訪问 1-1000 以外的下标相应的 值将返回nil。你能够依据须要定义数组的下标从 0,1 或者随意其它的数值開始,比方:
-- creates anarray with indicesfrom -5 to 5 a = {} for i=-5, 5 do a[i] = 0 end
然而在 Lua 中习惯上数组的下表从 1 開始,Lua 的标准库与此习惯保持一致。因此 假设你的数组下标也是从 1開始你就能够直接使用标准库的函数,否则就无法直接使用。
我们能够用构造器在创建数组的同一时候并初始化数组:
squares = {1, 4, 9, 16, 25,36, 49, 64,81}
这种语句中数组的大小能够随意的大,甚至几百万。
10.2 阵和多维数组
Lua 中主要有两种表示矩阵的方法,第一种是用数组的数组表示。也就是说一个表的元素是还有一个表。
比如,能够使用以下代码创建一个 n 行 m 列的矩阵:
mt = {} --create the matrix for i=1,N do mt[i] = {} --create a newrow for j=1,M do mt[i][j] = 0 end end
因为 Lua 中 table 是个对象,所以对于每一行我们必须显式的创建一个 table,这看 起来比起 c 或者 pascal 显得冗余,还有一方面它也提供了很多其它的灵活性。比如能够改动前 面的样例来创建一个三角矩阵:
for j=1,M do
改成
for j=1,i do
这样实现的三角矩阵比起整个矩阵。只使用一半的内存空间。 第二中表示矩阵的方法是将行和列组合起来,假设索引下标都是整数。通过第一个索引乘于一个常量C列)再加上第二个索引。看以下的样例实现创建 n 行 m 列的矩阵:
mt = {} --create the matrix for i=1,N do for j=1,M do mt[i*M + j] = 0 end end
假设索引都是字符串的话,能够用一个单字符将两个字符串索引连接起来构成一个 单一的索引下标。比如一个矩阵m。索引下标为 s和 t,假定 s和 t 都不包括冒号。代码 为:m[s..':'..t],假设 s 或者 t 包括冒号将导致混淆,比方("a:", "b")和("a", ":b")。当对这样的情况有疑问的时候能够使用控制字符来连接两个索引字符串,比方' '。
实际应用中常用稀疏矩阵,稀疏矩阵指矩阵的大部分元素都为空或者 0的矩阵。
比如。我们通过图的邻接矩阵来存储图,也就是说:当 m,n 两个节点有连接时,矩阵的m,n 值为相应的 x,否则为 nil。
假设一个图有 10000 个节点。平均每一个节点大约有 5 条 边,为了存储这个图须要一个行列分别为 10000 的矩阵。总计 10000*10000 个元素,实 际上大约仅仅有 50000 个元素非空(每行有五列非空,与每一个节点有五条边相应)。
非常多数 据结构的书上讨论採用何种方式才干节省空间,可是在 Lua中你不须要这些技术,由于 用table实现的数据本身天生的就具有稀疏的特性。
假设用我们上面说的第一种多维数组来表示。须要10000 个table。每一个 table 大约须要五个元素(table);假设用另外一种表示 方法来表示。仅仅须要一张大约 50000 个元素的表。无论用那种方式。你仅仅须要存储那些 非 nil 的元素。
10.3 链表
Lua 中用 tables非常easy实现链表,每个节点是一个 table,指针是这个表的一个域, 而且指向还有一个节点(table)。
比如。要实现一个仅仅有两个域:值和指针的基本链表,代 码例如以下:
根节点:
list = nil
在链表开头插入一个值为 v 的节点: list = {next = list, value = v}要遍历这个链表仅仅须要:
local l = listwhile l do print(l.value) l = l.next end其它类型的链表。像双向链表和循环链表类似的也是非常easy实现的。
然后在 Lua 中 在非常少情况下才须要这些数据结构,由于通常情况下有更简单的方式来替换链表。
比方, 我们能够用一个非常大的数组来表示枝,当中一个域 n指向枝顶。
10.4 队列和双端队列
尽管能够使用 Lua 的 table 库提供的 insert 和 remove 操作来实现队列。但这样的方式 实现的队列针对大数据量时效率太低,有效的方式是使用两个索引下标。一个表示第一个元素。还有一个表示最后一个元素。
function ListNew () return {first = 0,last = -1} end
为了避免污染全局命名空间,我们重写上面的代码,将其放在一个名为list 的 table
中:
List = {} function List.new () return {first = 0,last = -1} end
以下,我们能够在常量时间内,完毕在队列的两端进行插入和删除操作了。
function List.pushleft (list, value) local first = list.first - 1 list.first = first list[first] =value end function List.pushright (list, value) local last = list.last + 1 list.last= last list[last] =value end function List.popleft (list) local first = list.first if first > list.last then error("list is empty") end local value = list[first] list[first] = nil -- toallow garbage collection list.first =first + 1 return value end function List.popright (list) local last = list.last if list.first > last then error("list is empty") end local value = list[last] list[last] = nil -- toallow garbage collection list.last = last - 1 return value end
对严格意义上的队列来讲。我们仅仅能调用pushright 和 popleft,这样以来。first 和 last的索引值都随之添加,幸运的是我们使用的是 Lua 的 table实现的。你能够訪问数组的元素,通过使用下标从1到 20,也能够 16,777,216 到 16,777,236。另外。Lua 使用双精度 表示数字。假定你每秒钟执行 100 万次插入操作,在数值溢出曾经你的程序能够执行 200 年。
10.5 集合和包
假定你想列出在一段源码中出现的全部标示符,某种程度上,你须要过滤掉那些语言本身的保留字。
一些 C 程序猿喜欢用一个字符串数组来表示,将全部的保留字放在 数组中,对每个标示符到这个数组中查找看是否为保留字,有时候为了提高查询效率, 对数组存储的时候使用二分查找或者 hash 算法。
Lua 中表示这个集合有一个简单有效的方法,将全部集合中的元素作为下标存放在一个 table 里,以下不须要查找 table,仅仅须要測试看对于给定的元素。表的相应下标的 元素值是否为 nil。比方:
reserved = { ["while"] = true, ["end"] = true, ["function"] = true, ["local"] = true, } for w in allwords() do if reserved[w] then -- `w' is a reservedword ...
还能够使用辅助函数更加清晰的构造集合:
function Set (list) local set = {} for _, l in ipairs(list) do set[l] = true end return set end reserved = Set{"while", "end", "function", "local", }
10.6 字符串缓冲
假定你要拼接非常多个小的字符串为一个大的字符串,比方,从一个文件里逐行读入 字符串。你可能写出以下这种代码:
-- WARNING: badcode ahead!! local buff = "" for line in io.lines() do buff = buff ..line .. " " end
虽然这段代码看上去非常正常。但在 Lua中他的效率极低,在处理大文件的时候。你会明显看到非常慢,比如,须要花大概 1 分钟读取 350KB 的文件。
(这就是为什么 Lua 专 门提供了 io.read(*all)选项。她读取相同的文件仅仅须要 0.02s)
为什么这样呢?Lua 使用真正的垃圾收集算法。但他发现程序使用太多的内存他就 会遍历他全部的数据结构去释放垃圾数据,普通情况下,这个算法有非常好的性能(Lua 的快并不是偶然的)。可是上面那段代码loop 使得算法的效率极其低下。
为了理解现象的本质,假定我们身在loop中间。buff 己经是一个 50KB 的字符串, 每一行的大小为20bytes。当 Lua 运行 buff..line.." "时,她创建了一个新的字符串大小为 50,020 bytes,而且从 buff 中将 50KB 的字符串复制到新串中。也就是说,对于每一行,都要移动 50KB 的内存。而且越来越多。读取 100 行的时候C只 2KB),Lua己经移动 了 5MB 的内存。使情况变遛的是以下的赋值语句:
buff = buff ..line .. " "
老的字符串变成了垃圾数据,两轮循环之后,将有两个老串包括超过 100KB 的垃圾 数据。这个时候Lua 会做出正确的决定,进行他的垃圾收集并释放 100KB的内存。问题 在于每两次循环 Lua就要进行一次垃圾收集。读取整个文件须要进行 200 次垃圾收集。
而且它的内存使用是整个文件大小的三倍。
这个问题并非 Lua 特有的:其他的採用垃圾收集算法的而且字符串不可变的语言 也都存在这个问题。Java是最著名的样例,Java 专门提供 StringBuffer 来改善这样的情况。
在继续进行之前,我们应该做个凝视的是,在普通情况下,这个问题并不存在。
对 于小字符串,上面的那 个循环没有 不论什么问题。
为了读取整 个文件我们 能够使用 io.read(*all)。能够非常快的将这个文件读入内存。
可是在某些时候,没有解决问题的简单 的办法,所以以下我们将介绍更加高效的算法来解决问题。
我们最初的算法通过将循环每一行的字符串连接到老串上来解决这个问题,新的算法避 免如此:它连接两个小串成为一个略微大的串。然后连接略微大的串成更大的串。
。。算 法的核心是:用一个枝,在枝的底部用来保存己经生成的大的字符串,而小的串从枝定 入枝。
枝的状态变化和经典的汉诺塔问题类似:位于枝以下的串肯定比上面的长。仅仅要 一个较长的串入枝后比它以下的串长。就将两个串合并成一个新的更大的串,新生成的 串继续与相邻的串比較假设长于底部的将继续进行合并,循环进行到没有串能够合并或者到达枝底。
function newStack () return {""} -- starts withan empty string end function addString (stack, s) table.insert(stack, s) -- push's' into thethe stack for i=table.getn(stack)-1, 1, -1 do if string.len(stack[i]) > string.len(stack[i+1]) then break end stack[i] =stack[i] .. table.remove(stack) end end
要想获取终于的字 符串 ,我们仅仅须要从上向下一次合并全部的字符串就可以。table.concat 函数能够将一个列表的全部串合并。 使用这个新的数据结构,我们重写我们的代码:
local s = newStack() for line in io.lines() do addString(s, line.. " ") end s =toString(s)
终于的程序读取 350KB 的文件仅仅须要 0.5s,当然调用 io.read("*all")仍然是最快的 仅仅须要 0.02s。
实际上,我们调用 io.read("*all")的时候。io.read 就是使用我们上面的数据结构,仅仅 只是是用 C 实现的,在 Lua标准库中。有些其它函数也是用 C实现的,比方 table.concat。 使用 table.concat 我们能够非常easy的将一个table 的中的字符串连接起来,由于它使用 C 实现的,所以即使字符串非常大它处理起来速度还是非常快的。
Concat 接受第二个可选的參数,代表插入的字符串之间的分隔符。
通过使用这个參 数,我们不须要在每一行之后插入一个新行:
local t = {} for line in io.lines() do table.insert(t, line) end s =table.concat(t, " ") .. " "
io.lines 迭代子返回不带换行符的一行,concat 在字符串之间插入分隔符。可是最后 一字符串之后不会插入分隔符,因此我们须要在最后加上一个分隔符。最后一个连接操 作复制了整个字符串,这个时候整个字符串可能是非常大的。我们能够使用一点小技巧。插入一个空串:
table.insert(t, "") s =table.concat(t, " ")