Hatena::ブログ(Diary)

Hatena::Diary::pi8027

 | 

2011-08-17

部分末尾再帰

自分自身を二回以上呼び出すような再帰構造を持つ再帰関数に関しての末尾再帰というのはあまり考えないが、実は部分的に末尾再帰であるような例を考える事ができる。このような再帰の事を、部分末尾再帰と呼ぶ事としよう。(勝手に名付けた。一般的な呼び方があったら教えてください。)

このような関数ついて、正の整数 n を受け取り、フィボナッチ数列の n 番目を返す関数(fib 関数)を例にとり考えてみる。

Haskellツリー構造再帰を使った fib 関数の定義は、以下のようになる。

fib :: Int -> Int
fib n
    | n <= 1 = n
    | otherwise = fib (n - 1) + fib (n - 2)

この fib 関数は n が1以下であれば n をそのまま返し、そうでなければ fib (n - 1) と fib (n - 2) の和を返す。

つまり、n が0以上であるような fib (n + 2) は fib n + fib (n + 1) に簡約されるはずである。これが末尾再帰でないのは明らかだ。

しかし、ツリー構造の再帰であっても fib ... が fib ... の形に簡約されていれば外側の再帰に関しては末尾再帰になっていると言えるはずである。fib をその形に書き換えると、以下のようになる。

fib' :: Int -> Int -> Int
fib' sum n
    | n <= 1 = sum + n
    | otherwise = fib' (fib' sum (n - 2)) (n - 1)

この fib' 関数は、元の n 以外にもとある時点での計算結果(和)を持っている。n が0以上であるような fib' sum (n + 2) は fib' (fib' sum n) (n + 1) に簡約されるので、外側の再帰に関して末尾再帰であると言える。

また、スタックで関数呼び出しを管理する普通の言語であれば、fib' は fib に比べて再帰一回分のスタック消費を基準とした時のスタック消費量が約半分になるはずである。スタックの深さが見えるように fib 7 と fib' 7 をそれぞれ可視化すると以下のようになる。(上が fib 7、下が fib' 7)

  • f:id:pi8027:20110817130500p:image
  • f:id:pi8027:20110817130501p:image

syaminosyamino 2011/08/17 13:35 0  1  2   3   4
a  b a+b
   a  b  a+b
      a   b  a+b
こんな図をイメージして,

fib' :: Int -> Int -> Int -> Int
fib' a b n
    | n == 0 = a
    | otherwise = fib' b (a + b) (n - 1)

fib n = fib' 0 1 n
という定義にしているのをよく見ます。

自分自身を2回以上呼び出すような再帰関数でも,Double recursionである場合とそうでない場合があるらしいです。よくわかってませんが。

pi8027pi8027 2011/08/17 14:18 はい。それが一番良い書き方だと思います。
本当は fib よりも適切で単純な例が欲しかったのですが、良い例が思い浮かばなかったので。

pi8027pi8027 2011/08/17 14:42 手続き型言語でクイックソート書くとこれが適用できるんじゃないかという声がありました。良いかもしれない。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/pi8027/20110817/1313554140
 |