Lua 中的递归
实现递归算法,并理解 Lua 的尾调用优化。
Lua 中的递归 是 CoddyKit 上的免费 Lua Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Lua Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Lua Academy 课程共包含 4 节课。
什么是递归
递归是指函数通过调用自身来解决同一问题的更小实例。每个递归函数都需要一个基准情况(停止的位置)和一个递归情况(缩小问题规模的位置)。没有基准情况,递归就会导致栈溢出。
local function countdown(n)
if n <= 0 then
print("Blast off!")
return
end
print(n)
countdown(n - 1)
end
countdown(5)
-- 5, 4, 3, 2, 1, Blast off!阶乘
阶乘是递归的经典示例:n! = n × (n-1)!,基准情况为 0! = 1。每次调用都会使 n 减 1,直到达到基准情况。结果会沿着调用栈逐层“展开”返回,并在返回过程中不断相乘。
local function factorial(n)
if n <= 1 then return 1 end
return n * factorial(n - 1)
end
print(factorial(1)) -- 1
print(factorial(5)) -- 120
print(factorial(10)) -- 3628800斐波那契数列
斐波那契数列也是一个经典示例:fib(n) = fib(n-1) + fib(n-2)。朴素递归的时间复杂度是指数级的,因为它会反复计算相同的值。我们稍后会介绍使用记忆化来改进它,但现在先了解基本形式。
local function fib(n)
if n <= 1 then return n end
return fib(n - 1) + fib(n - 2)
end
for i = 0, 10 do
io.write(fib(i) .. " ")
end
print()
-- 0 1 1 2 3 5 8 13 21 34 55记忆化递归
记忆化会将之前计算出的结果缓存到一个表中。每次调用时,先检查缓存。如果缓存中已有该值,就立即返回。否则,计算该值并将其存储起来。这样可以将斐波那契数列的指数级时间复杂度变为线性时间。
local memo = {}
local function fib(n)
if memo[n] then return memo[n] end
if n <= 1 then return n end
local result = fib(n-1) + fib(n-2)
memo[n] = result
return result
end
print(fib(40)) -- 102334155 (fast!)尾调用优化
Lua 会执行尾调用优化(TCO):当函数的最后一个操作是调用另一个函数时,Lua 会复用当前的栈帧。这使得尾递归函数只使用 O(1) 的栈空间。并非所有递归都是尾递归——该调用必须是函数中最后执行的操作。
-- Tail-recursive factorial using accumulator
local function factTail(n, acc)
acc = acc or 1
if n <= 1 then return acc end
return factTail(n - 1, n * acc) -- tail call
end
print(factTail(10)) -- 3628800
-- Can handle very large n without stack overflow树遍历
递归非常适合表达树遍历。树是一个包含 value、left 和 right 字段的表。先序、中序和后序遍历的区别,仅在于相对于递归调用,处理值的时机不同。
local function inorder(node)
if node == nil then return end
inorder(node.left)
io.write(node.value .. " ")
inorder(node.right)
end
local tree = {
value=4,
left={value=2, left={value=1}, right={value=3}},
right={value=6, left={value=5}, right={value=7}}
}
inorder(tree) -- 1 2 3 4 5 6 7相互递归
两个函数可以相互调用,这称为相互递归。在 Lua 中,您需要进行前向声明:先声明局部变量,再为这些变量赋值为函数。这样,每个函数都可以引用对方的变量,因为该变量已经处于作用域内。
local isEven, isOdd
isEven = function(n)
if n == 0 then return true end
return isOdd(n - 1)
end
isOdd = function(n)
if n == 0 then return false end
return isEven(n - 1)
end
print(isEven(10)) -- true
print(isOdd(7)) -- true递归深拷贝
递归非常适合处理嵌套结构。深拷贝一个表意味着复制它的内容,并递归复制其中嵌套的表(这样修改副本就不会影响原表)。
local function deepCopy(orig)
local copy
if type(orig) == "table" then
copy = {}
for k, v in pairs(orig) do
copy[deepCopy(k)] = deepCopy(v)
end
setmetatable(copy, getmetatable(orig))
else
copy = orig
end
return copy
end
local a = {1, {2, 3}}
local b = deepCopy(a)
b[2][1] = 99
print(a[2][1]) -- 2 (unchanged)泛洪填充算法
泛洪填充(用于绘图程序和游戏地图)天然适合使用递归。从一个单元格开始,先标记它,然后递归填充每个尚未访问的邻居。当遇到边界或已访问的单元格时,递归就会终止。
local grid = {
{0,0,0,1,0},
{0,1,0,1,0},
{0,1,1,1,0},
{0,0,0,0,0},
}
local function fill(g, r, c)
if r<1 or r>#g or c<1 or c>#g[r] then return end
if g[r][c] ~= 0 then return end
g[r][c] = 2 -- mark visited
fill(g,r-1,c); fill(g,r+1,c)
fill(g,r,c-1); fill(g,r,c+1)
end
fill(grid, 1, 1)
print(grid[1][1], grid[2][1]) -- 2 2了解栈深度
Lua 的默认调用栈大小有限(非尾调用通常约为 200 层)。深度非尾递归会导致“栈溢出”错误。解决方案包括:改写为尾递归、使用显式栈(表),或使用协程处理大型迭代任务。
-- Simulate deep recursion danger
local function depth(n)
if n == 0 then return "done" end
return depth(n - 1) -- tail call, safe
end
print(depth(100000)) -- done (tail call, no overflow)
-- Non-tail: limited depth
local function countDown(n)
if n == 0 then return 0 end
return 1 + countDown(n - 1) -- NOT a tail call
end
-- countDown(10000) would stack overflow快速检查
以下哪一项是 Lua 中正确的尾调用?
回顾:递归
总结:
- 每个递归函数都需要基准情况和递归情况
- 尾调用(最后一个操作是调用)会由 Lua 优化——栈空间为 O(1)
- 记忆化可以将指数级递归转换为线性复杂度
- 相互递归需要前向声明
- 对树或图进行深度递归很自然;对于非尾调用,请注意栈深度
常见问题解答
「Lua 中的递归」课时是免费的吗?
是的 — 「Lua 中的递归」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Lua Academy 课程的其余内容,请升级到 CoddyKit PRO。 Lua Academy 课程共包含 4 节课。
「Lua 中的递归」这节课中我会学到什么?
实现递归算法,并理解 Lua 的尾调用优化。 你通过在浏览器中直接运行的动手代码来练习 Lua Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Lua Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Lua Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「Lua 中的递归」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Lua Academy 课中编写并运行代码吗?
能。每节 Lua Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 定义和调用函数
- 多个返回值
- 可变参数和 ... 运算符
- Lua 中的递归