0Pricing
Lua Academy · レッスン

Luaの再帰

再帰アルゴリズムを実装し、Luaの末尾呼び出し最適化を理解します。

「Luaの再帰」はCoddyKit上の無料Lua Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応の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

相互再帰

2つの関数が互いを呼び出すことを相互再帰といいます。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レベル)。深い非末尾再帰を行うと、「stack overflow」エラーが発生します。解決策として、末尾再帰に変換する、明示的なスタック(テーブル)を使う、大規模な反復処理にはコルーチンを使う方法があります。

-- 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の再帰」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Lua Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Lua Academyコースには全4レッスンが含まれています。

「Luaの再帰」で何を学びますか?

再帰アルゴリズムを実装し、Luaの末尾呼び出し最適化を理解します。 ブラウザで直接実行するハンズオンコードでLua Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Lua Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのLua Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。

「Luaの再帰」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このLua Academyレッスンでコードを書いて実行できますか?

はい。すべてのLua Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. 関数の定義と呼び出し
  2. 複数の戻り値
  3. 可変長引数と...演算子
  4. Luaの再帰
← Lua Academyに戻る