M.Hiroi's Home Page

Common Lisp Programming

お気楽 Common Lisp プログラミング入門

[ PrevPage | Common Lisp | NextPage ]

配列

「配列 (array)」はプログラミングを経験したことがある人ならばお馴染みのデータ構造でしょう。Common Lisp の配列は S 式であればどんなデータでも格納することができます。

●配列の生成

配列は関数 make-array を使って生成します。

make-array dimensions :initial-element item

キーワード引数 :initial-element は配列の要素の初期値を指定します。引数 dimensions は非負の整数を要素とするリストを与えます。そのリストの長さが配列の次元を表し、各要素がその次元のサイズを表します。たとえば次の図を見てください。

配列型データは数値や文字列と同じく自己評価フォームです。# の次の数字が次元を表し、A が配列であることを表します。とくに、1 次元の配列を「ベクタ (vector)」といいます。ベクタを生成する場合、引数はリストではなく整数値を与えてもかまいません。ベクタは #( ... ) で表示されます。

Common Lisp の場合、配列をリストのように表示しているので、ちょっとわかりにくいかもしれません。リストの階層構造と配列の次元を比較してください。

上図に示すように、第 1 次元はリストのトップレベルに対応し、第 2 次元は格納されているリストに対応します。つまり、次元の数だけリストが入れ子になっているのです。dimensions を (2 3 4) とした場合は、最初のリストが 2 つのリストを格納し、そのリストが 3 つのリストを格納し、さらにそのリストが 4 つの要素を格納していることに対応します。

それから、配列は表記法と同じ構文で生成することもできます。

* #(1 2 3 4 5 6 7 8)

#(1 2 3 4 5 6 7 8)
* #2A((1 2) (3 4))

#2A((1 2) (3 4))

●要素のデータ型と初期値の指定

make-array にはいろいろなキーワード引数が用意されています。:initial-element を含むキーワード引数を一切指定しない場合、要素の初期化は処理系に依存します。SBCL の場合、make-array dimensions だけだと要素は 0 に初期化されます。他の処理系、たとえば CLISP では NIL に初期化されます。

初期値の指定は :initial-element だけではなく、:initial-contents でも行うことができます。次の例を見てください。

* (make-array 5)

#(0 0 0 0 0)
* (make-array 5 :initial-element 'a)

#(A A A A A)
* (make-array 5 :initial-contents '(1 2 3 4 5))

#(1 2 3 4 5)
* (make-array '(2 2) :initial-contents '((1 2) (3 4)))

#2A((1 2) (3 4))

:initial-contents を使うと、各要素に異なった初期値を設定することができます。

配列の要素のデータ型はキーワード引数 :element-type で指定することができます。デフォルトの値は T で、S 式であれば何でも OK です。たとえば、:element-type に number を指定すれば要素は数に、integer であれば整数、string であれば文字列になると思ったのですが、実際の動作は処理系に依存しているようです。SBCL の場合、:element-type だけ指定しても、配列の要素は 0 に初期化されます。

* (make-array 8 :element-type 'symbol)

#(0 0 0 0 0 0 0 0)
* (setq a (make-array 8 :element-type 'symbol))

#(0 0 0 0 0 0 0 0)
* (array-element-type a)

T
> (make-array 8 :element-type 'integer)
#(NIL NIL NIL NIL NIL NIL NIL NIL)
> (setq a (make-array 8 :element-type 'integer))
#(NIL NIL NIL NIL NIL NIL NIL NIL)
> (array-element-type a)
T

関数 array-element-type は配列に格納できるデータの型指定子を返します。T が返されたので、:element-type で指定したデータ型以外のデータでも格納することができます。SBCL の場合、:initial-element と :element-type を同時に指定するとき、矛盾がないかチェックされます。

* (setq b (make-array 8 :element-type 'symbol :initial-element 'a))

#(A A A A A A A A)
* (setq b (make-array 8 :element-type 'symbol :initial-element 1))

=> エラー "Value of '1 in (THE SYMBOL '1) is 1, not a SYMBOL."
* (make-array 8 :element-type 'symbol :initial-element 1)

#(1 1 1 1 1 1 1 1)

配列を生成してもそれを捨てるときにはチェックされないようです。プログラムをコンパイルするとき、処理系によっては最適化などの処理で :element-type を考慮しているのかもしれません。

●配列のアクセス

配列からデータを取り出すには関数 aref を使います。

aref array subscripts ...

subscripts (添字) の個数は配列 array の次元と等しくて、それぞれの添字は次元の範囲内に収まっていなければいけません。

簡単な例を示しましょう。

* (aref #(10 20 30) 0)

10
* (aref #(10 20 30) 2)

30
* (aref #2A((10 20 30) (40 50 60)) 0 0)

10
* (aref #2A((10 20 30) (40 50 60)) 1 2)

60

Lisp の配列はC言語と同じく、添字は 0 から順番に数えます。2 次元配列の場合、(aref #2A(...) 0 0) では最初の 0 で (10 20 30) を指定し、次の 0 で要素 10 を指定します。(aref #2A(...) 1 2) では最初の 1 で (40 50 60) を指定して、次の 2 で要素 60 を指定します。

昔の Lisp で配列に値を代入する場合、配列専用の関数が用意されていた処理系がありましたが、Common Lisp では setf を使います。setf は変数に値を代入するだけではなく、配列にも値を代入することができるのです。Common Lisp では、データの代入は setf で統一することを推薦しています。setf の構文を示します。

setf アクセス関数 データ

アクセス関数は評価したときにデータを取り出す関数のことです。配列の場合、aref がアクセス関数になります。変数は評価するとその値を返すので、アクセス関数とみなすことができます。setf はアクセス関数が示す位置へデータを代入します。この機能を「汎変数」といいます。

簡単な例を示しましょう。

* (setq a (make-array 3))

#(0 0 0)
* (setf (aref a 1) 10)

10
* a

#(0 10 0)
* (setq a (make-array '(2 3)))

#2A((0 0 0) (0 0 0))
* (setf (aref a 1 2) 10)

10
* a

#2A((0 0 0) (0 0 10))

seft による代入は配列を直接書き換えること、つまり「破壊的」であることに注意してください。

●ベクタの生成

Common Lisp にはベクタを生成する関数 vector も用意されています。

vector item ...

vector はリストを生成する関数 list とよく似ています。引数を格納したベクタを返します。

* (vector)

#()
* (vector 1 2 3)

#(1 2 3)
* (vector 1 2 3 4 5 6 7 8)

#(1 2 3 4 5 6 7 8)
* (vector 'a 'b 'c 'd 'e)

#(A B C D E)

●ベクタとスタック

Lisp の場合、スタックはリストを使って簡単に実現できます。実際 Common Lisp には、リストをスタックとして操作するマクロ push と pop が用意されています。Lisp の主役はリストですが、配列 (ベクタ) でもスタックを実現することができます。

ベクタでスタックを実現する場合、データを格納するためのベクタ本体と、スタックのトップを表す変数が必要になります。この変数のことを「スタックポインタ (stack pointer)」と呼びます。次の図を見てください。

まず、配列 buffer とスタックポインタ top を用意します。top の値は 0 に初期化しておきます。データをプッシュするときは (aref buffer top) にデータを格納してから、top の値をインクリメントします。逆にポップするときは、top の値をデクリメントしてから、(aref buffer top) にあるデータを取り出します。スタックを操作するたびに、top の値は上図のように変化します。

データをプッシュしていくと、top の値は配列の大きさと等しくなります。配列はリストと違って大きさに限りがあるので、これ以上データを追加することはできません。つまり、スタックは満杯となります。したがって、データをプッシュするとき、スタックに空きがあるかチェックする必要があります。また、top が 0 のときはスタックが空の状態なので、ポップすることはできません。このチェックも必要になります。

●ベクタによるスタックの実装

実際のプログラムは次のようになります。

リスト : ベクタによるスタックの実装

(defvar *stack-size*)
(defvar *top*)
(defvar *stack*)

;;; 初期化
(defun init-stack (n)
  (setq *stack-size* n
        *top*        0
        *stack*      (make-array n)))

;;; データをスタックに積む
(defun push-down (data)
  (when (< *top* *stack-size*)
    (setf (aref *stack* *top*) data)
    (incf *top*)))

;;; スタックからデータを取り出す
(defun pop-up ()
  (when (plusp *top*)
    (decf *top*)
    (aref *stack* *top*)))

関数 init-stack は大きさ n のスタックを初期化します。ここでは、スタック本体とサイズ、スタックポインタをスペシャル変数として定義しています。関数 push-down は、最初にスタックに空きがあるかチェックしてから、データをスタックに格納します。スタックからデータを取り出す関数 pop-up では、スタックにデータがあることを確認します。

簡単な実行例を示します。

* (init-stack 5)

#(0 0 0 0 0)
* (dotimes (x 5) (push-down x))

NIL
* (push-down 5)

NIL
* (dotimes (x 5) (print (pop-up)))

4
3
2
1
0
NIL
* (pop-up)

NIL

●vector-push と vector-pop

このように、ベクタをスタックとして操作する関数は簡単に作ることができます。実をいうと、Common Lisp には関数 vector-push と vector-pop が用意されていて、ベクタをスタックとして簡単に操作することができます。

vector-push item vector
vector-pop vector

vector-push はスタックとして利用するベクタ vector に ITEM をプッシュします。スタックが満杯で item をプッシュできない場合は NIL を返します。逆に、vector-pop は vector からデータをポップします。データをポップできない場合はエラーとなります。

これらの関数は単なるベクタに適用することはできません。make-array でベクタを生成するときに、キーワード :fill-pointer に NIL 以外の値を指定することが必要です。指定できる値は 0 からベクタのサイズまでの整数値と T です。T を指定するとベクタのサイズと同じ値になります。この値がフィルポインタ (スタックポインタ) の初期値となります。フィルポインタの値が 0 だとスタックは空の状態で、スタックが満杯になるとフィルポインタはベクタのサイズと同じ値になります。

簡単な使用例を示しましょう。

* (setq stack (make-array 10 :fill-pointer 0))

#()
* (fill-pointer stack)

0
* (length stack)

0
* (vector-push 1 stack)

0
* stack

#(1)
* (fill-pointer stack)

1
* (length stack)

1
* (vector-pop stack)

1
* stack

#()

まず最初に、make-array で :fill-pointer を指定してベクタを生成します。この場合、0 を指定したのでスタックは空の状態です。ベクタの表示もフィルポインタの値に左右されます。関数 fill-pointer は、指定したベクタのフィルポインタの値を返します。

vector-push はフィルポインタの示す位置にデータを書き込み、フィルポインタの値をひとつ増やします。返り値はデータを書き込んだ位置、つまり、フィルポインタの前の値となります。なお、関数 length はベクタにも適用することができます。ベクタに :fill-pointer が設定されている場合はフィルポインタと同じ値を返します。

次に、vector-pop でデータを取り出します。vector-pop はフィルポインタの値をひとつ減らし、その位置に格納されているデータを返します。この場合、フィルポインタをひとつ減らすと 0 になり、そこに格納されている値 1 を返します。フィルポインタは 0 に戻ったので、スタックは空の状態となります。

●スタックの拡張

スタックをベクタで実装した場合、リストと違ってスタックのサイズには限りがあるので、vector-push を使う場合は返り値を確認しておいた方がよいでしょう。もしも、最初に設定したスタックのサイズを越えてデータをプッシュしたい場合は、関数 vector-push-extend を使います。この関数を使う場合、make-array でキーワード :adjustable に NIL 以外の値を指定し、ベクタの大きさを動的に変更できるように設定しておく必要があります。

vector-push-extend item vector &optional extension

ベクタを拡張する以外は vector-push と同じです。extension はベクタに追加する要素の個数を指定します。まあ、これは Lisp 処理系に任せておけばいいので、特に指定する必要はないでしょう。簡単な使用例を示します。

* (setq stack (make-array 3 :fill-pointer t :adjustable t))

#(0 0 0)
* (vector-push 1 stack)

NIL
* (vector-push-extend 1 stack)

3
* stack

#(0 0 0 1)

:adjustable に T を指定してベクタを生成します。:fill-pointer に T を指定したので、スタックは満杯の状態です。この場合、vector-push はデータをプッシュできないので NIL を返します。vector-push-extend はベクタを拡張するので、データをプッシュすることができます。

●素数を求める (2)

拙作のページ 繰り返し で素数を求めるプログラムを作りましたが、素数はリストに格納しました。このプログラムでは、新しい素数をリストの最後尾に追加するため append を使っていますが、append は第 1 引数のリストをコピーするので、リストが長くなると効率が悪くなります。そこで、リストではなくベクタに素数を格納してみましょう。次のリストを見てください。

リスト : 素数を求める (ベクタ版)

;; 素数のチェック
(defun primep (n ps)
  (dotimes (i (length ps) t)
    (let ((m (aref ps i)))
      (cond
       ((zerop (mod n m)) (return))
       ((< n (* m m)) (return t))))))

;; 素数のベクタを返す
(defun prime (n)
  (do ((ps (make-array 1 :fill-pointer t :adjustable t :initial-contents '(2)))
       (m 3 (+ m 2)))
      ((< n m) ps)
    (if (primep m ps)
        (vector-push-extend m ps))))

関数 prime では、大きさ 1 で要素が 2 のベクタを生成して変数 PS にセットします。このとき、:fill-pointer と :adjustable を T に指定します。素数を見つけたら vector-push-extend で PS に追加します。関数 primep では、dotimes で 0 から (length ps) - 1 までの添字を生成し、(aref ps i) でベクタから素数を取り出してチェックします。あとは特に難しいところはないと思います。

それでは実際に試してみましょう。

* (time (length (prime 100000)))

Evaluation took:
  0.025 seconds of real time
  0.015625 seconds of total run time (0.015625 user, 0.000000 system)
  64.00% CPU
  60,625,956 processor cycles
  213,680 bytes consed

9592

実行環境 : Ubunts 18.04 (Windows Subsystem for Linux), Intel Core i5-6200U 2.30GHz

リスト版が 0.326 秒でしたが、ベクタ版は 0.025 秒になりました。とても速くなりましたね。求める素数の個数が多くなるほど、その差は大きくなると思われます。興味のある方はいろいろ試してみてください。

●問題

問題1

下記に示すデータの度数分布表と累積度数表を求めるプログラムを作ってください。

リスト : 身長のデータ

(defvar *height*
  #(148.7 149.5 133.7 157.9 154.2 147.8 154.6 159.1 148.2 153.1
    138.2 138.7 143.5 153.2 150.2 157.3 145.1 157.2 152.3 148.3
    152.0 146.0 151.5 139.4 158.8 147.6 144.0 145.8 155.4 155.5
    153.6 138.5 147.1 149.6 160.9 148.9 157.5 155.1 138.9 153.0
    153.9 150.9 144.4 160.3 153.4 163.0 150.9 153.3 146.6 153.3
    152.3 153.3 142.8 149.0 149.4 156.5 141.7 146.2 151.0 156.5
    150.8 141.0 149.0 163.2 144.1 147.1 167.9 155.3 142.9 148.7
    164.8 154.1 150.4 154.2 161.4 155.0 146.8 154.2 152.7 149.7
    151.5 154.5 156.8 150.3 143.2 149.5 145.6 140.4 136.5 146.9
    158.9 144.4 148.1 155.5 152.4 153.3 142.3 155.3 153.1 152.3))
   階級   度数 累積度数
------------------------
130 - 135   1      1
135 - 140   6      7
140 - 145  12     19
145 - 150  25     44
150 - 155  32     76
155 - 160  17     93
160 - 165   6     99
165 - 170   1    100

階級はデータの範囲を表します。この表では x cm 以上 y cm 未満を x - y で表しています。度数はその階級に出現したデータの個数です。度数を示してある表のことを「度数分布表」といいます。累積度数はその階級までの度数を全部加えたものです。累積度数を示してある表を「累積度数分布表」といいます。

問題2

問題1のデータで、平均値と標準偏差を求めるプログラムを作ってください。データを x1, x2, ... , xN とすると、総計量 (合計値) T と平均値 M は次式で求めることができます。

\( \begin{eqnarray} T &=& x_1 + x_2 + \cdots + x_N = \displaystyle \sum_{i=1}^{N} x_i \\ M &=& \dfrac{x_1 + x_2 + \cdots + x_N}{N} = \dfrac{1}{N} \sum_{i=1}^{N} x_i \end{eqnarray} \)

平均値が同じ場合でも、データの特徴が異なる場合があります。たとえば、A = {4, 4, 5, 5, 5, 6, 6, 6, 7, 7} と B = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} の平均値は 5.5 になります。A のデータは平均値の近くに集まっていてますが、B のデータはバラバラになっていますね。統計学では、ばらつきの大きさを表すために「分散 (variance)」という値を使います。分散 \(S^2\) の定義を次に示します。

\( S^2 = \dfrac{(x_1-M)^2 + (x_2-M)^2 + \cdots + (x_N-M)^2}{N} = \dfrac{1}{N} \displaystyle \sum_{i=1}^{N} (x_i-M)^2 \)

分散の定義からわかるように、平均値から離れたデータが多いほど、分散の値は大きくなります。逆に、平均値に近いデータが多くなると分散は小さな値になります。そして、分散の平方根 \(S = \sqrt{S^2} \) が「標準偏差 (SD : standard deviation)」になります。

問題3

ベクタ xs の中から最大値を求める関数 maximum xs と最小値を求める関数 minimum xs を定義してください。

問題4

ベクタを使って「パスカルの三角形」を表示するプログラムを作ってください。

問題5

ベクタを使ってフィボナッチ数を高速に求めるプログラムを作ってください。













●解答1

リスト : 度数分布表と累積度数表

(defun get-class (x limit)
  (dotimes (i (length limit))
    (if (and (>= (first (aref limit i)) x)
             (< x (second (aref limit i))))
        (return i))))

(defun frequency ()
  (let ((limit #((130 135) (135 140) (140 145) (145 150)  ; 境界値
                 (150 155) (155 160) (160 165) (165 170)))
        (freq  (make-array 8 :initial-element 0))         ; 度数
        (cum   (make-array 8)))                           ; 累積度数

    ;; 度数分布表の作成
    (dotimes (i (length *height*))
      (incf (aref freq (get-class (aref *height* i) limit))))

    :: 累積度数表の作成
    (setf (aref cum 0) (aref freq 0))
    (dotimes (i 7)
      (setf (aref cum (1+ i))
            (+ (aref cum i) (aref freq (1+ i)))))

    ;; 表示
    (dotimes (i 8)
      (format t "~D - ~D | " (first (aref limit i)) (second (aref limit i)))
      (format t "~3D ~3D~%"  (aref freq i) (aref cum i)))))

配列 FREQ が度数分布表、CUM が累積度数表、LIMIT が下限値と上限値を表します。FREQ は 0 で初期化します。度数分布表の作成は簡単です。最初の dotimes で *HEIGHT* の要素を取り出し、関数 get-class で階級 (0 - 7) を求めます。get-class は引数 X が範囲内となる LIMIT の添字を求めるだけです。そして、incf でその階級の度数を +1 します。incf や decf も汎変数が使えるので便利です。

累積度数表の作成も簡単です。(aref cum 0) を (aref freq 0) で初期化します。あとは、度数分布表の値 (aref freq (1+ i)) と累積度数表の値 (aref cum i) を足し算していくだけです。最後に、dotimes で 2 つの表を出力します。

実行結果は次のようになります。

* (frequency)
130 - 135 |   1   1
135 - 140 |   6   6
140 - 145 |  12  12
145 - 150 |  25  25
150 - 155 |  32  32
155 - 160 |  17  17
160 - 165 |   6   6
165 - 170 |   1   1
NIL

●解答2

リスト : 平均値と標準偏差

(defun sum (xs &aux (a 0d0))
  (dotimes (i (length xs) a) (incf a (aref xs i))))

(defun meansd (xs)
  (let ((m (/ (sum xs) (length xs)))
        (s 0d0))
    (dotimes (i (length xs) (list m (sqrt (/ s (length xs)))))
      (incf s (expt (- (aref xs i) m) 2)))))

関数 meansd は平均値と標準偏差をリストに格納して返します。公式をそのままプログラムしただけなので、説明は不要でしょう。

* (meansd *height*)

(150.62699981689454d0 6.433472932259082d0)

平均値は 150.63 cm で、標準偏差は 6.43 cm になりました。

●解答3

リスト : 最大値と最小値

(defun maximum (xs)
  (do ((i 1 (1+ i))
       (m (aref xs 0)))
      ((>= i (length xs)) m)
      (if (< m (aref xs i))
          (setf m (aref xs i)))))

(defun minimum (xs)
  (do ((i 1 (1+ i))
       (m (aref xs 0)))
      ((>= i (length xs)) m)
      (if (> m (aref xs i))
          (setf m (aref xs i)))))
* (maximum *height*)

167.9
* (minimum *height*)

133.7

プログラムは簡単です。XS の先頭要素を変数 M に格納します。そして、do ループで 1 番目から順番に要素を取り出して M と比較します。(aref xs i) が M よりも大きい (または M よりも小さい) 場合は M の値を (aref xs i) に書き換えます。最後に M の値を返すだけです。

●解答4

n 段の三角形を作る場合、大きさが n + 1 のベクタを用意します。たとえば、6 段の三角形を作ってみましょう。次の図を見てください。


    図 : パスカルの三角形の求め方

最初にベクタの内容を 0 に初期化し、1 番目に 1 をセットします。これが 1 段目になります。あとは図のように、隣の要素を足し算するだけです。プログラムは次のようになります。

リスト : パスカルの三角形 (ベクタ版)

(defun pascal (n)
  (let ((buff (make-array (1+ n) :initial-element 0)))
    (setf (aref buff 1) 1)
    (dotimes (i n)
      (do ((j (1+ i) (1- j)))
          ((zerop j))
        (format t "  ~3D" (setf (aref buff j)
                                (+ (aref buff j) (aref buff (1- j))))))
      (terpri))))

ベクタはひとつあれば十分です。ただし、ベクタの値を書き換えていくので、ベクタの後方から計算していくことに注意してください。前方から計算すると、値がおかしくなります。実行結果は次のようになります。

* (pascal 10)
    1
    1    1
    1    2    1
    1    3    3    1
    1    4    6    4    1
    1    5   10   10    5    1
    1    6   15   20   15    6    1
    1    7   21   35   35   21    7    1
    1    8   28   56   70   56   28    8    1
    1    9   36   84  126  126   84   36    9    1
NIL

●解答5

フィボナッチ数の定義と二重再帰のプログラムを再掲します。

\( fibo(n) = \begin{cases} 0, & if \ n = 0 \\ 1, & if \ n = 1 \\ fibo(n - 1) + fibo(n - 2), & if \ n \gt 1 \end{cases} \)
リスト : フィボナッチ関数 (二重再帰版)

(defun fibo (n)
  (if (< n 2)
      n
    (+ (fibo (1- n)) (fibo (- n 2)))))

関数 fibo は同じ値を何度も計算しているので、実行時間はとても遅くなります。そこで、計算した値は表に格納しておいて、2 回目以降は表から計算結果を求めるようにします。このような方法を「表計算法」といいます。この処理は引数 N とベクタの添字を対応させることで実現できます。つまり、ベクタの N 番目には、(fibo n) の結果を格納しておくわけです。プログラムは次のようになります。

リスト : フィボナッチ数 (ベクタ版)

;; スペシャル変数の定義
(defvar *fibo-table*
  (make-array 2 :fill-pointer t :adjustable t :initial-contents '(0 1)))

;; 本体
(defun fibo-sub (n)
  (do ((i (length *fibo-table*) (1+ i)))
      ((> i n) (aref *fibo-table* n))
      (vector-push-extend (+ (aref *fibo-table* (1- i))
                             (aref *fibo-table* (- i 2)))
                          *fibo-table*)))

;; フィボナッチ数
(defun fibo (n)
  (if (< n (length *fibo-table*))
      (aref *fibo-table* n)
    (fibo-sub n)))

ベクタはスペシャル変数 *fibo-table* にセットします。ベクタの大きさは 2 で、要素は (0 1) に初期化します。関数 fibo は引数 N が (length *fibo-table*) 未満であれば (aref *fibo-table* n) を返します。そうでなければ、関数 fibo-sub を呼び出して N のフィボナッチ数を求めます。

fibo-sub は (length *fibo-table*) の値を変数 I にセットし、do ループで I から引数 N までのフィボナッチ数を順番に求めます。I 未満のフィボナッチ数は *fibo-table* に格納されているので、I - 1 番目と I - 2 番目の値を足し算して、それを *fibo-table* に追加していくだけです。最後に (aref *fibo-table* n) を返します。

それでは実行してみましょう。

* (dotimes (x 20) (print (fibo x)))

0
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
NIL
* (fibo 40)

102334155
* *fibo-table*

#(0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 10946
  17711 28657 46368 75025 121393 196418 317811 514229 832040 1346269 2178309
  3524578 5702887 9227465 14930352 24157817 39088169 63245986 102334155)

二重再帰の場合、(fibo 40) は 3.6 秒もかかりますが、今回のプログラムはすぐに値を返します。二回目以降は表を引くだけなので、ほとんど時間はかかりません。

ところで、フィボナッチ数は直前の 2 項の値がわかれば計算できるので、それを変数に保持しておけば、単純な繰り返しでも高速に計算することができます。プログラムは次のようになります。

リスト : フィボナッチ数 (繰り返し版)

(defun fibo-iter (n)
  (do ((a 0 b)
       (b 1 (+ a b))
       (i 0 (1+ i)))
      ((= i n) a)))

変数 A と B を 0 と 1 に初期化します。A が I 番目のフィボナッチ数を、B が I + 1 番目のフィボナッチ数を表します。あとは do ループで I を +1 するたびに、A の値を B に、B の値を (+ a b) に更新するだけです。

do の場合、変数の更新処理は逐次実行ではありません。B を (+ a b) で更新するとき、A の値は B に書き換えられてはいません。すべての更新処理を評価してから、変数の値が更新されます。ご注意くださいませ。

それでは実行してみましょう。

* (fibo-iter 40)

102334155
* (fibo-iter 100)

354224848179261915075

100 のフィボナッチ数も瞬時に求めることができます。


Copyright (C) 2020 Makoto Hiroi
All rights reserved.

[ PrevPage | Common Lisp | NextPage ]