2-1、Lua數據結構

2-1、Lua數據結構


table是Lua中唯一的數據結構,其他語言所提供的數據結構,如:arrays、records、lists、queues、sets等,Lua都是通過table來實現,並且在lua中table很好的實現了這些數據結構。
在傳統的C語言或者Pascal語言中我們經常使用arrays和lists(record+pointer)來實現大部分的數據結構,在Lua中不僅可以用table完成同樣的功能,而且table的功能更加強大。通過使用table很多算法的實現都簡化了,比如你在lua中很少需要自己去實現一個搜索算法,因爲table本身就提供了這樣的功能。

1、數組

在lua中通過整數下標訪問table中元素,即是數組。並且數組大小不固定,可動態增長。
通常我們初始化數組時,就間接地定義了數組的大小,例如:

a = {}		-- new array
for i=1, 1000 do
	a[i] = 0
end

數組a的大小爲1000,訪問1-1000範圍外的值,將返回nil。數組下標可以根據需要,從任意值開始,比如:

-- creates an array with indices from -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}

這樣的語句中,數組的大小可以任意的大。

2、矩陣和多維數組

Lua中有兩種表示矩陣的方法,一是“數組的數組”。也就是說,table的每個元素是另一個table。例如,可以使用下面代碼創建一個n行m列的矩陣:

mt = {}			-- create the matrix
for i=1,N do
	mt[i] = {}		-- create a new row
	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

這樣實現的三角矩陣比起整個矩陣,僅使用一半的內存空間。
表示矩陣的另一方法,是將行和列組合起來。如果索引下標都是整數,通過第一個索引乘於一個常量(列)再加上第二個索引,看下面的例子實現創建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’。
實際應用中常常使用稀疏矩陣,稀疏矩陣指矩陣的大部分元素都爲空或者0的矩陣。例如,我們通過圖的鄰接矩陣來存儲圖,也就是說:當m,n兩個節點有連接時,矩陣的m,n值爲對應的x,否則爲nil。如果一個圖有10000個節點,平均每個節點大約有5條邊,爲了存儲這個圖需要一個行列分別爲10000的矩陣,總計10000*10000個元素,實際上大約只有50000個元素非空(每行有五列非空,與每個節點有五條邊對應)。很多數據結構的書上討論採用何種方式才能節省空間,但是在Lua中你不需要這些技術,因爲用table實現的數據本身天生的就具有稀疏的特性。如果用我們上面說的第一種多維數組來表示,需要10000個table,每個table大約需要五個元素(table);如果用第二種表示方法來表示,只需要一張大約50000個元素的表,不管用那種方式,你只需要存儲那些非nil的元素。

3、鏈表

Lua中用tables很容易實現鏈表,每一個節點是一個table,指針是這個表的一個域(field),並且指向另一個節點(table)。例如,要實現一個只有兩個域:值和指針的基本鏈表,代碼如下:
根節點:

list = nil

在鏈表開頭插入一個值爲v的節點:

list = {next = list, value = v}

要遍歷這個鏈表只需要:

local l = list
while l do
	print(l.value)
	l = l.next
end

其他類型的鏈表,像雙向鏈表和循環鏈表類似的也是很容易實現的。然後在Lua中在很少情況下才需要這些數據結構,因爲通常情況下有更簡單的方式來替換鏈表。比如,我們可以用一個非常大的數組來表示棧,其中一個域n指向棧頂。

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		-- to allow 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		-- to allow 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年。

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 reserved word
	...

還可以使用輔助函數更加清晰的構造集合:

function Set (list)
	local set = {}
	for _, l in ipairs(list) do set[l] = true end
	return set
end

reserved = Set{"while", "end", "function", "local", }

6、字符串緩衝

假定你要拼接很多個小的字符串爲一個大的字符串,比如,從一個文件中逐行讀入字符串。你可能寫出下面這樣的代碼:

-- WARNING: bad code 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行的時候(僅僅2KB),Lua已經移動了5MB的內存,使情況變遭的是下面的賦值語句:

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

老的字符串變成了垃圾數據,兩輪循環之後,將有兩個老串包含超過100KB的垃圾數據。這個時候Lua會做出正確的決定,進行他的垃圾收集並釋放100KB的內存。問題在於每兩次循環Lua就要進行一次垃圾收集,讀取整個文件需要進行200次垃圾收集。並且它的內存使用是整個文件大小的三倍。
這個問題並不是Lua特有的:其它的採用垃圾收集算法的並且字符串不可變的語言也都存在這個問題。Java是最著名的例子,Java專門提供StringBuffer來改善這種情況。
在繼續進行之前,我們應該做個註釋的是,在一般情況下,這個問題並不存在。對於小字符串,上面的那個循環沒有任何問題。爲了讀取整個文件我們可以使用io.read(*all),可以很快的將這個文件讀入內存。但是在某些時候,沒有解決問題的簡單的辦法,所以下面我們將介紹更加高效的算法來解決這個問題。
我們最初的算法通過將循環每一行的字符串連接到老串上來解決問題,新的算法避免如此:它連接兩個小串成爲一個稍微大的串,然後連接稍微大的串成更大的串。。。算法的核心是:用一個棧,在棧的底部用來保存已經生成的大的字符串,而小的串從棧定入棧。棧的狀態變化和經典的漢諾塔問題類似:位於棧下面的串肯定比上面的長,只要一個較長的串入棧後比它下面的串長,就將兩個串合併成一個新的更大的串,新生成的串繼續與相鄰的串比較如果長於底部的將繼續進行合併,循環進行到沒有串可以合併或者到達棧底。

function newStack ()
	return {""}	-- starts with an empty string
end

function addString (stack, s)
	table.insert(stack, s)	-- push 's' into the the 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")
發佈了116 篇原創文章 · 獲贊 130 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章