第十一課 Lua實現相關數據結構

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








































發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章