第8回 リスト(6月5日)

今日の課題

■ ペア

ペアとは2つのデータをまとめた構造体のことである。ドットペアと呼ばれることもある。

ペアに対する基本操作は以下の3つである:
意味 関数名 形式
ペアの作成 cons (cons 〈データ1〉 〈データ2〉)
ペアの左側の抽出 car (car 〈ペア〉)
ペアの右側の抽出 cdr (cdr 〈ペア〉)

例題1

以下を Definitions に記述し、Run してみよう。
(define p1 (cons 100 200))
(car p1)
(cdr p1)

■リスト

ペアを組み合わせて、データを並べた構造体をリストと呼ぶ。

たとえば、次のようにペアを使うと、数字のリストになる。
(define p2 (cons 1 (cons 2 (cons 3 (cons 4 ())))))

() は空を表す

このように、リストの正体は、ペアの集りであるが、分りにくい。そこで、次のように書くことができる。
(define p3 (list 1 2 3 4))
または
(define p3 '(1 2 3 4))

演習では印刷上のわかりやすさから、クオートの表記はせず、list の表記を用いる。

equal? を使って p2 と p3 が等しいことを確認しよう。
> (equal? p2 p3)

リストに対する基本操作を以下にまとめる:
意味 関数名 形式
リストの作成 list (list 〈データ1〉 〈データ2〉 … 〈データn〉)
リストの先頭に要素を追加 cons (cons 〈要素〉 〈リスト〉)
リストから先頭要素を抽出 car (car 〈リスト〉)
リストから2番目以降のリストを抽出 cdr (cdr 〈リスト〉)
リストが空かどうかの検査 null? (null? 〈リスト〉)

幾つかリスト操作を確認しよう。
> (list 1 2 3)
> (cons 1 (list 5 4 3))
> (car (list 1 2 3))
> (cdr (list 1 2 3))
> (null? (list 1 2 3))
> (null? (list ))
> (cdr (list 1))
> (null? (cdr (list 1)))  ※ この方法で、リストの要素数が1つかどうかの検査が可能
> (define x (list 1 2 3)) ※ 名前を付けることができる
> (car x)
> (cdr x)

よく使う組み込みの関数

下記の動作を確認しよう。


> (length (list 1 2 3 4 5))
> (append (list 1 2 3) (list 4 5 6))

例題2

リストを扱う関数を作ってみよう。

  1. 数を要素とするリストを入力し、その数の総和を求める関数 list-sum

    実行方法: (list-sum (list 1 2 3 4 5))

    予想結果: 15

    簡単な解答例:
    *** 解1 ***
    (define list-sum
      (lambda (lst)
        (if (null? lst)
            0
            (+ (car lst) (list-sum (cdr lst))))))    
    

    末尾再帰関数として作る場合:
    *** 解2 ***
    (define list-sum2
      (lambda (lst n)
        (if (null? lst)
            n
            (list-sum2 (cdr lst) (+ n (car lst))))))
    
    (define list-sum
      (lambda (lst)
        (list-sum2 lst 0)))
    

    解2を変更して、関数定義を内部に持たせる場合:
    *** 解3 ***
    (define list-sum
      (lambda (lst)
        (define list-sum2
          (lambda (lst n)
            (if (null? lst)
                n
                (list-sum2 (cdr lst) (+ n (car lst))))))
        (list-sum2 lst 0)))
    

    ※ 解2は、Interactions 上で list-sum2 を実行することができるが、解3は、実行できない。

  2. 要素数が1個以上の数値のリストを入力として、最大値を求める関数 list-max

    実行方法: (list-max (list 1 2 4 5 3))

    予想結果: 5




    簡単だけど愚かな解答例:
    *** 解1 ***
    (define list-max
      (lambda (lst)
        (if (null? (cdr lst))                     要素数が1つか?
            (car lst)                             先頭要素を返す
            (if (> (car lst) (list-max (cdr lst)) 先頭要素と2番目以降の最大値を比較
                (car lst)                         先頭要素を返す
                (list-max (cdr lst))))))          2番目以降の最大値を返す
    

    2つめの if 文で、list-max の計算をするが、もし、その値のほうが大きいとき、2つ下の式が評価されるのだが、再び同じ計算をすることになる。

    無駄な計算を省いた解答例:
    *** 解2 ***
    (define greater
      (lambda (m n)
        (if (> m n) m n)))
    (define list-max
      (lambda (lst)
        (if (null? (cdr lst))
            (car lst)
            (greater (car lst) (list-max (cdr lst))))))
    

    最後の行で、リストの2番目以降の最大値を、まず求めており、その結果が greater で使われているので、同じ計算を繰り返すことはない。

    一方、末尾再帰関数として作成してみると、もっと簡単になる。
    *** 解3 ***
    (define list-max2
      (lambda (lst n)        これから計算すべきリスト lst、これまでの最大値 n
        (if (null? lst)      リストが空のとき、
            n                これまでの最大値 n が全体の最大値
            (if (> n (car lst))  そうでなければ、先頭要素とこれまでの最大値を比較
                (list-max2 (cdr lst) n)               n が大きいとき
                (list-max2 (cdr lst) (car lst))))))  先頭要素が大きいとき
    
    (define list-max
      (lambda (lst)
        (list-max2 (cdr lst) (car lst)))) 
                                 先頭要素をこれまでの最大値とし、2番目以降を処理
    

    list-max2 の定義を内部に持たせる場合:
    *** 解4 ***
    (define list-max
      (lambda (lst)
        (define list-max2
          (lambda (lst n)
            (if (null? lst)
                n
                (if (> n (car lst))
                    (list-max2 (cdr lst) n)
                    (list-max2 (cdr lst) (car lst))))))
          (list-max2 (cdr lst) (car lst))))
    

例題3

例題2では、リストから値を抽出する関数であった。次にリストを返す関数を作ってみよう。

  1. append 関数は、drscheme に組み込み関数として既に用意されている。ここでは練習のために、自分で cons を用いて定義しよう。関数名は my-append としよう。

    実行方法: (my-append (list 1 2 3) (list 4 5 6))

    予想結果: (1 2 3 4 5 6)
    (define my-append
      (lambda (lst1 lst2)
        (if (null? lst1)
            lst2
            (cons (car lst1) (my-append (cdr lst1) lst2)))))
    

    ※ append は、末尾再帰関数が簡単には作れません。

  2. 入力された数値のリストのうち3の倍数を「'*」に書き換えたリストを返す関数 replace-sanbai

    実行方法: (replace-sanbai (list 1 2 3 4 5 6 7 8 9 10))

    予想結果: (1 2 '* 4 5 '* 7 8 '* 10)
    (define replace-sanbai
      (lambda (lst)
        (if (null? lst)
            (list)         ← 空のリスト
            (if (= (modulo (car lst) 3) 0)
                (cons '* (replace-sanbai (cdr lst)))
                (cons (car lst) (replace-sanbai (cdr lst)))))))
    

  3. 入力された数値のリストに対して、各値を 2 倍としたリストを返す関数 list-double

    実行方法: (list-double (list 1 2 3 4 5))

    予想結果: (2 4 6 8 10)
    (define list-double
      (lambda (lst)
        (if (null? lst)
            (list)              ← 空のリスト
            (cons (* 2 (car lst)) (list-double (cdr lst))))))
    


■ 練習問題

練習1:ドリル

リスト操作と再帰呼出しに慣れるために、以下の関数を作ろう。

練習2:A4用紙のサイズ

よく使うプリンタの用紙は、A4サイズである(210mm x 297mm)。この用紙は、長辺を半分に折るとA5サイズの用紙になる(小数点の切り捨てはtruncate関数を使用)。A0サイズは、841mm x 1189mm という大きさである。では、nを変数として Anサイズの2辺の長さをペアの値で返す関数 (paper-a-size n) を定義せよ。

動作例:(paper-a-size 4) ⇒ (210 . 297)

練習3:連分数

表1には連分数の表記とその具体的な式の関係が示されている。たとえば、(list 2 4)は、2 + 1 / 4 という式の略記であり、(list 3 6 9) は、3 + 1 / (6 + 1 / 9) という式の略記である。そこで、入力される数のリストに対して連分数を計算する関数 contfrac を定義せよ。

表1 連分数の表記と計算式

練習4:ウノ


(c) 2008.6.3 by tokuhisa