您的位置:首页 > 理论基础 > 数据结构算法

第 10 章 数据结构

2016-02-25 15:24 441 查看
第 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 dofor 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'。

实际应用中常常使用稀疏矩阵,稀疏矩阵指矩阵的大部分元素都为空或者 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很容易实现链表,每一个节点是一个 table,指针是这个表的一个域, 并且指向另一个节点(table)。例如,要实现一个只有两个域:值和指针的基本链表,代 码如下:

根节点:

list = nil


在链表开头插入一个值为 v 的节点: list = {next = list, value = v}要遍历这个链表只需要:

local l = listwhile l do
   print(l.value)
    l = l.next
end
其他类型的链表,像双向链表和循环链表类似的也是很容易实现的。然后在 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 .. "\n"
end


尽管这段代码看上去很正常,但在 Lua中他的效率极低,在处理大文件的时候,你会明显看到很慢,例如,需要花大概 1 分钟读取 350KB 的文件。(这就是为什么 Lua
专 门提供了 io.read(*all)选项,她读取同样的文件只需要 0.02s)

为什么这样呢?Lua 使用真正的垃圾收集算法,但他发现程序使用太多的内存他就 会遍历他所有的数据结构去释放垃圾数据,一般情况下,这个算法有很好的性能(Lua 的快并非偶然的),但是上面那段代码loop 使得算法的效率极其低下。

为了理解现象的本质,假定我们身在loop中间,buff 己经是一个 50KB 的字符串, 每一行的大小为20bytes,当 Lua 执行 buff..line.."\n"时,她创建了一个新的字符串大小为 50,020 bytes,并且从 buff 中将 50KB 的字符串拷贝到新串中。也就是说,对于每一行,都要移动 50KB 的内存,并且越来越多。读取 100 行的时候C仅仅 2KB),Lua己经移动 了 5MB 的内存,使情况变遛的是下面的赋值语句:

buff = buff ..line .. "\n"


老的字符串变成了垃圾数据,两轮循环之后,将有两个老串包含超过 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.. "\n")
end
s =toString(s)


最终的程序读取 350KB 的文件只需要 0.5s,当然调用 io.read("*all")仍然是最快的 只需要 0.02s。

实际上,我们调用 io.read("*all")的时候,io.read 就是使用我们上面的数据结构,只 不过是用 C 实现的,在 Lua标准库中,有些其他函数也是用 C实现的,比如 table.concat, 使用 table.concat 我们可以很容易的将一个table 的中的字符串连接起来,因为它使用 C 实现的,所以即使字符串很大它处理起来速度还是很快的。

Concat 接受第二个可选的参数,代表插入的字符串之间的分隔符。通过使用这个参 数,我们不需要在每一行之后插入一个新行:

local t = {}
for line in io.lines() do
    table.insert(t, line)
end
s =table.concat(t, "\n") .. "\n"

io.lines 迭代子返回不带换行符的一行,concat 在字符串之间插入分隔符,但是最后 一字符串之后不会插入分隔符,因此我们需要在最后加上一个分隔符。最后一个连接操 作复制了整个字符串,这个时候整个字符串可能是很大的。我们可以使用一点小技巧,插入一个空串:

table.insert(t, "")
s =table.concat(t, "\n")
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: