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

今日の課題

□ 言語の選択

本演習を実施するにあたり、言語の選択をやり直して下さい。

使用する言語: Pretty Big

メニューバーから次のとおりクリックする。「言語」→「言語の選択」→「(レガシーな言語)Pretty Big」

■ ペア

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

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

例1

以下を定義エリア(Definitions)に記述し、実行(Run)してみよう。
(define p1 (cons 100 200))
(car p1)
(cdr p1)

■リスト

C言語の演習などでもお馴染の「片方向の線形リスト」は、schemeではペアを組合せて作成する。

たとえば、次のようにペアを使うと、数字のリストになる。
(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 が等しいことを確認しよう。#t が返り値となるはずである。
> (equal? p2 p3)

1つのリストに対する基本操作を以下にまとめる:
意味 関数名 形式
リストの作成 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)

■ 再帰呼出しとリスト操作

リストを操作するプログラムの多くは、再帰呼び出しを使う。簡単なものから練習しよう。

例2

数を要素とするリストを入力とし、その合計値を返す関数 sum を作成せよ。

(解答例)次の順番で考えるとよい。

  1. 関数名と引数を決める。
    (define sum
      (lambda (lst)
        ; ここにもっと書き込む予定 ... (イ)
        ))
    
  2. 再帰呼出しの終了条件を(イ)に書き込む。
        (if (null? lst)
          ; ここに、空リストのときの処理を書き込む予定 ... (ロ)
          ; ここに、リストに要素が残っているときの処理を書き込む予定 ) ... (ハ)
    
  3. 終了時の処理を(ロ)に書き込む。
          0    ; (∵ 空リストの合計値は 0)
    
  4. 途中の状態での処理を(ハ)に書き込む。
          (+ リストlstの先頭の要素  リストlstの2番目以降の要素列、すなわちリストの合計値)
    

    ここで、リストlstの先頭の要素は、(car lst) で得られ、

    リストlstの2番目以降の要素列は、(cdr lst) で得られる。

    (cdr lst) の合計値は、(sum (cdr lst)) である。

    したがって、(ハ)には、以下のように書き込む。
          (+ (car lst) (sum (cdr lst)))
    

練習1 (入力:リスト、出力:数値)

(1) 数を要素とするリストを入力とし、各要素を2乗して、その合計値を返す関数 sum2 を作成せよ。

(2) リストの要素数を数える関数 size を作成せよ。例えば (list 1 2 3 'ok 'ng) を入力すると 5 を返す。

練習2 (入力:リスト、出力:リスト)

(1) 数を要素とするリストを入力とし、各要素を2乗したリストを返す関数 pow2-list を作成せよ。例えば (list 1 2 3 4) を入力すると (list 1 4 9 16) を返す。

(2) 数を要素とするリストと数 d を入力とし、リストの各要素について d で割った余りを求める関数 mod-list を作成せよ。例えば (list 1 2 3 4 5 6 7) と 3 を入力すると (list 1 2 0 1 2 0 1) を返す。なお、modulo により余りを計算できる。

練習3 (入力:リストとリスト、出力:リスト)

(1) 2つのリスト lst1、lst2 を入力とし、lst1 の後に lst2 を接続したリストを返す関数 cat-list を作成せよ。例えば (list 1 2 3) と (list 4 5 6) を入力すると (list 1 2 3 4 5 6) を返す。

(2) 2つのリスト lst1、lst2 を入力とし、リストの要素を交互に要素とするリストを返す関数 merge-list を作成せよ。例えば (list 1 2 3) と (list 4 5 6) を入力すると (list 1 4 2 5 3 6) を返す。また、(list 1 2 3) と (list 4 5 6 7 8 9) を入力すると (list 1 4 2 5 3 6 7 8 9) を返す。

■ 末尾再帰関数

再帰関数は、再帰計算の結果に何か計算をして返り値とする場合と、再帰計算の結果をそのまま返り値とする場合とに分けられる。

前者は、上記の例題で紹介したとおりである。すなわち、(+ (car lst) (sum (cdr lst))) という計算は、(sum (cdr lst)) という再帰呼出しの結果を足し算に使用しており、その結果が返り値となっている。

後者は、末尾再帰関数と呼ばれる。数のリストの合計値を求める関数の例を以下に示す。
*** 解1 ***
(define sum00
  (lambda (lst n)
    (if (null? lst)
        n
        (sum00 (cdr lst) (+ n (car lst))))))

(define sum
  (lambda (lst)
    (sum00 lst 0)))

sum00 は、数のリスト lst と、これまでの合計値 n を入力とする。例えば、(list 1 2 3 4) を計算する場合、再帰呼出しの様子は次のようになる。

最初:(sum00 (list 1 2 3 4) 0)

再帰1: (sum00 (list 2 3 4) 1)

再帰2: (sum00 (list 3 4) 3)

再帰3: (sum00 (list 4) 6)

再帰4: (sum00 () 10)

なお、sum00 を sum のlambda 式の中に閉じ込めることができる。
*** 解2 ***
(define sum
  (lambda (lst)
    (define sum00
      (lambda (lst n)
        (if (null? lst)
            n
            (sum00 (cdr lst) (+ n (car lst))))))
    (sum00 lst 0)))

■ ドリル

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

(1) 数のリストを入力とし、最大値を返す関数 max-list

(2) 数のリストを入力とし、分散を返す関数 variancce-list

(3) 確率値のリストを入力とし、エントロピーを返す関数 entropy(ヒント:エントロピーは、-Σipi log2 pi である)

(4) 数のリストを入力とし、偶数値だけのリストを作って返す関数 even-list(ヒント:数値が偶数であることを判定する関数 even? は既に存在する。例、(even? 4) ⇒ #t)

(5) 長さの同じ2つの数のリストを入力とし、同じ番目の数の和を要素とするリストを返す関数 add-list(例、(add-list (list 1 2 3) (list 4 5 6)) の返り値は (list 5 7 9)である。)

(6) リストを入力とし、その順序を逆にしたリストを作って返す関数 rev-list(例、(rev-list (list 1 2 3 4)) の返り値は (list 4 3 2 1) である。)


(c) 2009.5.25 by tokuhisa