《Lua程序设计第四版》 第二部分14~17章自做练习题答案
Lua程序设计第四版第二部分编程实操自做练习题答案,带⭐为重点。
14.1 ⭐
该函数用于两个稀疏矩阵相加
function martixAdd(a, b)
local c = {}
for i = 1, #a, 1 do
c[i] = {}
for k, v in pairs(a[i]) do
c[i][k] = v
end
end
for i = 1, #b, 1 do
for k, v in pairs(b[i]) do
c[i][k] = (c[i][k] or 0) + v
c[i][k] = (c[i][k] ~= 0) and c[i][k] or nil
end
end
return c
end
A = {{
[5] = 1
}, {}, {
[1] = 3,
[3] = 4
}, {}, {
[4] = -1
}}
B = {{
[2] = 2
}, {}, {
[1] = -3,
[4] = -3
}, {
[3] = 8
}, {
[1] = 7
}}
for k, v in pairs(martixAdd(A, B)) do
for j, i in pairs(v) do
print(k .. "行", j .. "列", i)
end
end
14.2
改写队列的实现,使得当队列为空时两个索引都返回0
function listNew()
return {
_first = 0,
_last = -1,
first = function()
if _first > _last then
return 0
end
return _first
end,
last = function()
if _first > _last then
return 0
end
return _last
end
}
end
l = listNew()
-- 模拟空队列
l._first = 10
l._last = 9
print(l.first, l.last)
14.3
修改图所用的数据结构,使得图可以保存每条边的标签。该数据结构应该使用包括两个字段的对象来表示每一条边,即边的标签和边指向的节点。与邻接集合不同,每一个节点保存的是从当前节点出发的边的集合。
local function name2node(graph, name)
local node = graph[name]
if not node then
graph[name] = {
name = name,
edges = {}
}
node = graph[name]
end
return node
end
function readgraph(file)
local graph = {}
f = io.open(file, 'r')
for l in f:lines() do
local name1, name2, edgeLabel = string.match(l, "(%S+)%s+(%S+)%s+(%S+)")
local from = name2node(graph, name1)
local to = name2node(graph, name2)
table.insert(from.edges, {
["label"] = edgeLabel,
["adj"] = to
})
end
return graph
end
graph = readgraph("graph.txt")
for k, v in pairs(graph) do
for _, v in pairs(v.edges) do
print(k, v.label, v.adj.name)
end
end
[[
C 1 D
C 3 E
D 1 C
D 7 E
A 4 B
A 2 D
B 1 D
B 4 C
]]
A B 4
A D 2
B D 1
D C 1
C D 1
B C 4
D E 7
C E 3
14.4 ⭐
使用14.3的表示方式,其中边的标签代表两个终端节点之间的距离。该函数使用Dijkstra算法寻找两个指定节点之间的最短路径。
local function name2node(graph, name)
local node = graph[name]
if not node then
graph[name] = {
name = name,
edges = {}
}
node = graph[name]
end
return node
end
function readgraph(file)
local graph = {}
f = io.open(file, 'r')
for l in f:lines() do
local name1, name2, edgeLabel = string.match(l, "(%S+)%s+(%S+)%s+(%S+)")
local from = name2node(graph, name1)
local to = name2node(graph, name2)
table.insert(from.edges, {
["label"] = edgeLabel,
["adj"] = to
})
end
return graph
end
graph = readgraph("graph.txt")
for k, v in pairs(graph) do
for _, v in pairs(v.edges) do
print(k, v.label, v.adj.name)
end
end
function Dijkstra(graph, a, b)
-- 点不存在
if not graph[a] or not graph[b] then
return nil
end
local D = {}
local T = {}
-- D 加入起点
D[graph[a]] = 0
-- T 加入其他点,初始化可达最短距离为正无穷大
for name, node in pairs(graph) do
if name ~= a then
T[node] = math.maxinteger
end
end
-- 当D中不包含b点时循环
while not D[graph[b]] do
-- 根据 D 迭代 T 可达最短距离
for node, d in pairs(D) do
for _, edge in pairs(node.edges) do
if not D[edge.adj] then
local newD = d + tonumber(edge.label)
T[edge.adj] = math.min(T[edge.adj], newD)
end
end
end
-- 选择 T 可达距离最小的点
local tmp = nil
for node, d in pairs(T) do
tmp = tmp or node
if d < T[tmp] then
tmp = node
end
end
-- 如果没有点被选中,退出循环
if T[tmp] == math.maxinteger then
return nil
end
-- 将前面选择到的点加入D,并从T删除
D[tmp] = T[tmp]
T[tmp] = nil
end
return D[graph[b]]
end
d = Dijkstra(graph, "A", "E")
if not d then
print("无路径")
end
print(d)
-- 具体路径计算为 A->E 路径距离 - (X->E 路径距离) == A -> X 路径距离
-- 即 A->X->E
15.1~15.2
修改序列化函数,使其带缩进地输出嵌套表,并添加["key"]=value的语法
function serialize(o, tab)
tab = tab or 1
local t = type(o)
if t == "number" or t == "string" or t == "boolean" or t == "nil" then
io.write(string.format("%q", o))
elseif t == "table" then
io.write("{\n")
for k, v in pairs(o) do
io.write(string.rep(" ", tab))
io.write(" [", string.format("%s", serialize(k)), "] = ")
serialize(v, tab + 1)
io.write(",\n")
end
io.write(string.rep(" ", tab))
io.write("}")
else
error("cannot serialize a" .. t)
end
end
A = {
B = {
E = "dog",
H = {
F = "PIG",
G = "goose"
}
},
C = "apple",
D = 114514,
I = {1, 2, 3, {4, 5, 6}}
}
serialize(A)
[[
{
["C"] = "apple",
["D"] = 114514,
["I"] = {
[1] = 1,
[2] = 2,
[3] = 3,
[4] = {
[1] = 4,
[2] = 5,
[3] = 6,
},
},
["B"] = {
["E"] = "dog",
["H"] = {
["G"] = "goose",
["F"] = "PIG",
},
},
}
]]
15.3
修改15.2,使其只在必要时,当键为字符串而不是合法标识符时才使用形如["key"]=value的语法
function serialize(o, tab)
tab = tab or 1
local t = type(o)
if t == "number" or t == "string" or t == "boolean" or t == "nil" then
io.write(string.format("%q", o))
elseif t == "table" then
io.write("{\n")
for k, v in pairs(o) do
io.write(string.rep(" ", tab))
if type(k) == "string" and string.match(k, "[%a_]+[%w_]*") then
io.write(" ", k, " = ")
else
io.write(" [")
serialize(k)
io.write("] = ")
end
serialize(v, tab + 1)
io.write(",\n")
end
io.write(string.rep(" ", tab))
io.write("}")
else
error("cannot serialize a" .. t)
end
end
A = {
B = {
E = "dog",
H = {
F = "PIG",
G = "goose"
}
},
C = "apple",
D = 114514,
I = {1, 2, 3, {4, 5, 6}}
}
serialize(A)
[[
{
C = "apple",
D = 114514,
I = {
[1] = 1,
[2] = 2,
[3] = 3,
[4] = {
[1] = 4,
[2] = 5,
[3] = 6,
},
},
B = {
E = "dog",
H = {
G = "goose",
F = "PIG",
},
},
}
]]
15.4
使其在可能时使用列表的构造器语法。例如应将表{14,15,19}序列化为{14,15,19},而不是{[1] =14,[2]=15,[3]=19},在遍历该部分的时候不要重复序列化。
function serialize(o)
local typ = type(o)
if typ == "number" or typ == "string" or typ == "boolean" or typ == "nil" then
io.write(string.format("%q", o))
elseif typ == "table" then
io.write("{\n")
local hash = {}
-- 遍历列表并进行hash记录
for i, v in ipairs(o) do
serialize(v)
io.write(",")
hash[i] = true
end
_ = (#hash ~= 0) and io.write("\n") or nil
-- 遍历除列表之外的所有元素
for k, v in pairs(o) do
if type(k) == "string" and string.match(k, "[%a_]+[%w_]*") then
io.write(" ", k, " = ")
elseif hash[k] == true then
goto continue
-- do nothing
else
io.write(" [")
serialize(k)
io.write("] = ")
end
serialize(v)
io.write(",\n")
::continue::
end
io.write("}")
else
error("cannot serialize a" .. t)
end
end
A = {
B = {
E = "dog",
H = {
F = "PIG",
G = "goose"
}
},
C = "apple",
D = 114514,
I = {1, 2, 3, {4, 5, 6}},
[1] = 4,
[2] = 3,
[3] = 3,
[5] = 4
}
serialize(A)
TEST = {
4,
3,
3,
[5] = 4,
C = "apple",
D = 114514,
I = {1, 2, 3, {4, 5, 6}},
B = {
E = "dog",
H = {
G = "goose",
F = "PIG"
}
}
}
[[
{
4,3,3,
[5] = 4,
C = "apple",
D = 114514,
I = {
1,2,3,{
4,5,6,
},
},
B = {
E = "dog",
H = {
G = "goose",
F = "PIG",
},
},
}
]]
16.1
通常,在加载代码段时增加一些前缀很有用。该函数类似于函数load,不过会将第一个参数增加到待加载的代码段之前。
像原始的load函数一样,函数loadwithprefix应该既可以接收字符串形式的代码段,也可以通过函数进行读取。即使待加载的代码段是字符串形式的,函数loadwithprefix也不应该进行实际的字符串连接操作。相反,它应该调用函数load并传入一个恰当的读取函数来实现功能,这个读取函数首先返回要增加的代码,然后返回原始的代码段。
function loadWithPrefix(s, f)
local tmp = 1
return load(function()
if tmp == 1 then
tmp = 2
return s
elseif tmp == 2 then
if type(f) == "string" then
tmp = 3
return f
elseif type(f) == "function" then
return f()
end
else
return nil
end
end)
end
f = loadWithPrefix("local x = 100;", "print(x)")
f()
f = loadWithPrefix("local x = 100;", io.lines('load.txt', '*L'))
f()
16.2 ⭐
请编写一个函数mulitiload,该函数接收一组字符串或函数来生成函数。其他规则与16.1类似。
function multiLoad(...)
local t = {...}
local i = 1
return load(function()
::continue::
if i <= #t then
local v = t[i]
if type(v) == "string" then
i = i + 1
return v
elseif type(v) == "function" then
local tmp = v()
if tmp then
return tmp
else
i = i + 1
goto continue -- 进行下一回合
end
end
else
return nil
end
end)
end
f = multiLoad("local x = 100;", "print(x)", "x = 10; print(x);")
f()
f = multiLoad("local x = 100;", io.lines('load.txt', '*L'), "x = 10; print(x);")
f()
16.3 ⭐
该函数对于指定的n返回特定版本的函数stringrep_n。在实现方面不能使用闭包,而是应该构造出包含合理指令序列的Lua代码,然后再使用函数load生成最终的函数。请比较通用版本的stringrep函数与我们自己实现的版本之间的性能差异
自己实现的版本重复次数是固定的,省去了计算各组合次数的性能开销,速度更快。
function stringrepN(n)
local top = [[local s = ...;local r = "";]]
local middle = ""
local tail = "r=r..s;return r;"
local count = 0
local tmp = n
while n // 2 ~= 0 do
count = count + 1
n = n // 2
middle = middle .. 's=s..s;'
end
for i = 1, tmp - 2 ^ count, 1 do
top = top .. 'r=r..s;'
end
return load(top .. middle .. tail)
end
stringrep = stringrepN(256)
start = os.clock()
stringrep("abcdefg", 2 ^ 25)
print(os.clock() - start)
start = os.clock()
string.rep("abcdefg", 2 ^ 25)
print(os.clock() - start)
print(string.rep("a", 256) == stringrep("a"))
[[
0.0
0.321
true
]]
16.4
你能想到一个使pcall(pcall,f)的第1个返回值为false的f?
function b(x)
print(x)
end
-- 一个函数和要传入的参数
ok, msg = pcall(b, 100)
print(ok, msg)
io.write("\n")
ok, msg = pcall(nil) -- 有错误信息但未引发错误
print(ok, msg)
io.write("\n")
ok, msg = pcall(pcall, nil) -- 错误信息返回了false
print(ok, msg)
io.write("\n")
ok, msg = pcall(pcall, (function()
end)()) -- 连nil也不返回的空value
print(ok, msg)
io.write("\n")
17.2
将几何区域系统的实现,9.4重写为恰当的模块
mod = require 'mod'
circle = mod.disk(0, 0, 0.5)
circle2 = mod.translate(circle, -0.3, 0.2)
shape = mod.difference(circle, circle2)
local plot = mod.plot
plot(shape, 512, 512, "test.pbm")
M = {}
function M.disk(cx, cy, r)
return function(x, y)
return (x - cx) ^ 2 + (y - cy) ^ 2 <= r ^ 2
end
end
function M.rect(left, right, top, bottom)
return function(x, y)
return left <= x and x <= right and bottom <= y and y <= top
end
end
function M.complement(r)
return function(x, y)
return not r(x, y)
end
end
function M.union(r1, r2)
return function(x, y)
return r1(x, y) or r2(x, y)
end
end
function M.intersection(r1, r2)
return function(x, y)
return r1(x, y) and r2(x, y)
end
end
function M.difference(r1, r2)
return function(x, y)
return r1(x, y) and not r2(x, y)
end
end
function M.translate(r, dx, dy)
return function(x, y)
return r(x - dx, y - dy)
end
end
function M.plot(r, M, N, file)
f = io.open(file, "w")
f:write("P1\n", M, " ", N, "\n")
for i = 1, N, 1 do
local y = (N - i * 2) / N
for j = 1, M do
local x = (j * 2 - M) / M
f:write(r(x, y) and "1" or "0")
end
f:write("\n")
end
f:close()
end
return M
17.3
如果在lua的搜索路径中添加一项绝对路径的文件名,会导致require一个不存在的模块时,这个模块都会搜索到固定文件名的文件作为它导入的模块
而且模块名不同,在package.loaded都会缓存,加载相同固定路径的module,在内存中也是两份独立的副本
在极端情况下有用,相当于始终会有默认的模块被加载,可以用这套机制来拓展require函数
https://www.lua.org/pil/8.1.html
17.4
编写一个同时搜索lua文件和C标准库的搜索器
function searcher(file, path)
if type(file) ~= "string" or type(path) ~= "string" then
return nil, error("not string type")
end
file = package.searchpath(file, path)
if not file then
return nil, error("no file")
end
f = loadfile(file)
if not f then
f = package.loadlib(file)
end
if not f then
return nil, error("failed load")
end
return f(), nil
end
local m = searcher("mod", "./?.lua;./?.so;/usr/lib/lua5.2/?.so;/usr/share/lua5.2/?.lua")
print(m.rect(1, 1, 1, 1))