Document

5. 任意長の合成データ:リスト
プログラミング論I
本日の内容
1.リスト:有限だが不定数の要素を持つ
(参考:構造体は要素数が確定している)
– 事前に要素数を確定できないデータを処理する場合
2. Scheme プログラムでのリストの記法
リスト関連:cons list first rest empty? list?
3.任意数の要素を含むリストの再帰的データ定義
4.リストを処理する再帰的な関数の設計法
リストとは
• 不定数のデータの並び(合成データ)
• データの並びには順序があり操作は先頭側から
要
素
を
加
え
だ
ん
だ
ん
長
く
23, 32, 6, 8 の順にリストに加える例
終端
最初は空
23
23を加える
32を加える
32
23
6
32
23
6
32
23
同様に続く…
先頭側:後から
加えた側。操作
はこちらから
8
終端側:
先に加え
た要素は
こちら側
リストの作成:cons
空リスト emptyから始め cons を用いてリストを作成
(cons 追加する先頭要素 追加されるリスト)
empty
(cons 23 empty)
(cons 32
(cons 23 empty))
最初は空
23を加える
23
32を加える
32
23
6
32
23
6
32
23
…
同様に…
(cons 8
(cons 6
8
(cons 32
(cons 23 empty))))
Scheme でのリストの記法
consを使うリストの表記 (長くて見にくい?)
8
6
32
23
終端
(cons 8 (cons 6 (cons 32 (cons 23 empty)))
listを使った省略記法(以降の説明ではこの記法)
(list 8 6 32 23)
要素データの並び
これ自体が1つのデータ値(式)
合成データなので複数の要素データを含み得る
C言語でのリストのプログラム
data *next
8
6
NULL
#include <stdio.h>
#include <stdlib.h>
typedef struct _node{
int data;
struct _node *next;
}node_t;
void printList(node_t *p){
while (p != NULL){
printf("data=%d\n", p->data);
p = p->next;
}
}
int main(){
node_t a, b, c, d;
a.data=8; a.next=&b;
b.data=6; b.next=&c;
c.data=32; c.next=&d;
d.data=23; d.next=NULL;
printList(&a);
return(0);
}
Schemeでは,上記を以下のように書ける
(list 8 6 32 23)
C言語でのリストのプログラム
data *next
8
6
NULL
#include <stdio.h>
#include <stdlib.h>
#define SUCCESS 1
#define FAILURE 0
typedef struct _node{
int data;
struct _node *next;
}node_t;
// newNode:int, node_t* -> node_t*
node_t *newNode(int d, node_t* p){
node_t *np=(node_t *) malloc(sizeof (node_t));
if (np == NULL){
fprintf(stderr, “Memory allocation error.”);
return NULL;
}
np->data = d;
np->next = p;
return np;
}
int prepend(int d, node_t **pp){
node_t = *p;
p = newNode(d, *pp);
if (p == NULL) return FAILURE;
*pp = p;
return SUCCESS;
}
void printList(node *p){
while (p != NULL){
printf(“%d “, p->data);
p = p->next;
}
}
void main(){
node_t *p=NULL;
prepend(23, &p);
prepend(32, &p);
prepend(6, &p);
prepend(8, &p);
Schemeでは,
printList(p);
上記を以下の
}
ように書ける
(list 8 6 32 23)
C言語でのリストのプログラム
data *next
8
6
NULL
#include <stdio.h>
#include <stdlib.h>
#define SUCCESS 1
#define FAILURE 0
typedef struct _node{
int data;
struct _node *next;
}node_t;
// newNode:int, node_t* -> node_t*
node_t *newNode(int d, node_t* p){
node_t *np=(node_t *) malloc(sizeof (node_t));
if (np == NULL){
fprintf(stderr, “Memory allocation error.”);
return NULL;
}
np->data = d;
np->next = p;
return np;
}
int append(int d, node_t **pp){
node_t = *p;
p = newNode(d, NULL);
if (p == NULL) return FAILURE;
while(*pp != NULL)
pp = &((*pp)->next);
*pp = p;
return SUCCESS;
}
void printList(node *p){
while (p != NULL){
printf(“%d “, p->data);
p = p->next;
}
}
void main(){
node_t *p=NULL;
append(23, &p);
append(32, &p);
append(6, &p);
Schemeでは,
append(8, &p);
上記を以下の
printList(p);
ように書ける
}
(list 23 32 6 8)
リスト要素の参照:first と rest
(構造体は定義に応じたフィールドのセレクタで要素参照)
リストの場合 → どのリストでもセレクタは first とrest
first
15
rest
8
6
32
23
終端
• first:先頭の要素を参照するセレクタ
(first (list 15 8 6 32 23)) は 15
• rest:先頭を除いた残りのリストを参照するセレクタ
(rest (list 15 8 6 32 23)) は(list 8 6 32 23)
例)上のリストの rest の rest の rest の first は:32
例題1.リストの first と rest の例
先のリストの rest の rest の rest の first は?
計算は最内括弧から行う点に注意
(first (rest (rest (rest (list 15 8 6 32 23)))))
first部 rest部
= (first (rest (rest (list 8 6 32 23))))
first部 rest部
= (first (rest (list
6 32 23)))
first部 rest部
= (first (list 32 23))
first部 rest部
= 32
実行結果
特別なリストemptyとリスト処理のエラー
empty は「空リスト」を表す特別な定数記号
(数でいう 0 のようなもの)
「(空でない)非空のリスト」だけ firstやrest が実行でき
る。 empty はリストだが要素参照はできない。
演算
非空リスト 空リスト
empty
リスト以外
(数など)
first OK
実行エラー
実行エラー
rest
実行エラー
実行エラー
OK
データの属性判定:list? empty?
演算と対象データの組合わせによってはエラーに!
→ 適切な属性判定により正しく処理を進行
• list? :リストであるか調べる
– 「リスト」ならば true (さもなければ false)
• empty? :空リストであるかを調べる
– 「空リスト」ならば true (さもなければ false)
リストに関するここまでのまとめ
•空リスト emptyから始め cons でより長いリストを
組み立てる
(cons x a_list) :a_listの先頭にx を加えたリスト
•要素の列挙でも記述できる:(list 要素の並び)
(例) (list 15 8 6 32 23)
•リストに関する他の演算子
(first a_list) はリスト a_list の先頭の要素
(rest a_list) はリスト a_list の先頭を除いた残り
(list? a_list) はリスト a_list がリストかどうか判断
(empty? a_list) はリスト a_list が空リストか判断
練習1
1. 先頭から1, 2, 3 の順に数が並ぶリスト Lを書け
2. 前述1のリスト Lの先頭に0を加える式を書け
3. 前述1のリスト Lのrest の rest の first を
求めよ。それは何番目の要素か?
4. 次の組合せの結果を書け(「エラー」ならエラーと記入)
L=(list 1) の場合 L=(list 1 2)
(first L) の結果
(rest L)
(first (rest L))
L=(list 1 2 3)
リストの再帰的データ定義と
再帰的なリスト処理関数の設計法
任意長であるリストの再帰的データ定義
リスト:不定要素数を含む合成データ
要素数を固定しない場合の定義は? →再帰
整数リストの例:すべての整数リストは以下のどちらか
* 最初の空リスト (空リストから開始して,より長いリストを構築)
* 整数と,他の整数リストを用いて作られた整数リスト
整数リスト loi (list-of-integers) は以下のいずれか
1. 空リスト empty, もしくは
2. (cons i loi) ここで i は整数、loi は整数リスト
定義はそれ自身を参照(整数リストが何かを整数リストで説明).
Self-Referential(自己参照) または Recursive(再帰的) と呼ぶ.
上記で2は自己参照を持つが,1は持たない(自己参照の停止).
プログラム設計法:再帰データの関数
再帰データを扱う関数向けに プログラム設計法を拡張
•データの解析と定義:
•対象は任意数の要素を持つ → 再帰的(自己参照)データ定義が必要 (少な
くとも2つの節を含み、少なくとも1つの節は自己参照してはならない)
• Purpose:プログラムの目的を理解、機能の概要を記述
•Contract, Header, Statement:
• Example:プログラムの振る舞いを「例」で記述.
• Template:
•データ定義と同数の節を持つcond式で定式化
•セレクタ式(rest)に対する自己適用(自然再帰Natural Recursion)
• Body Definition:
•まず基底ケース (再帰を含まないcond節)から開始
•次に自己参照のケース。再帰的適用に対し関数は目的記述文の通りに機
能すると想定→ あとは結果への値の結合についての問題。
• Test:検査を通じた誤り(エラー)の発見
リスト処理の再帰関数のテンプレート
リスト処理関数のテンプレート
節2つと自己参照1つを持つcond式が本体式
(define (fun-for-list a_list)
セレクタrestの結
(cond
果値へ自己適用
要素がなければ繰り返さない
(先頭をとった残りの
データ定 [(empty? a_list) …]
リストを繰返し処理)
義と同様 [else … (first a_list) …
2つの節
… (fun-for-list (rest a_list)) …]))
リスト処理の再帰関数の本体定義
まず基底ケース(再帰を含まないcond節)から開始
通常、対応する答えは定式化が容易か、あるいは例により既知。
次に自己参照のケースを扱う。
再帰的な適用 (関数は目的記述文通りに機能すると想定)
→ あとは必要に応じて値を結合することを検討。
結果値の生成(部分的な結果を結合し最終結果へ):
– 多くの場合、結合は基本処理(+ and consなど)で実現
– もし first項のデータに対して何らかのチェックが必要なら
入れ子のcondが必要なことが考えられる.
– 場合により補助関数を導入することも必要.
例題2. リストの長さ(要素数)
整数リストの要素数を求める関数 how-many
入力
how-many
整数のリスト
例:(list 11 12)
入力はリスト : 任意長の再帰データ
出力
数
例:2
リスト処理の再帰関数のテンプレート
リスト処理関数のテンプレート
2つの節と1つの矢印を持つcond式を持つ(テキスト表現
では矢印の代わりにセレクタ式への関数の自己適用)
整数のリストを処理する関数のテンプレート例
(define (fun-for-loi a_loi)
セレクタrestの結
果値へ自己適用
(cond 要素がなければ繰り返さない
(先頭をとった残りの
データ定 [(empty? a_loi) …]
リストを繰返し処理)
義と同様 [else … (first a_loi) …
2つの節
… (fun-for-loi (rest a_loi)) …]))
リスト処理の再帰関数の本体定義
例:関数 how-many (整数リストの要素数を計算する)
;; how-many : list-of-integers -> number
;; to determine how many integers are on a_loi
(define (how-many a_loi)
(cond [(empty? a_loi) …]
[else … (first a_loi) …
… (how-many (rest a_loi)) …]))
テンプレート
• 基底ケースの答は0 (空リストの要素数は0)
• 第2節はfirst項の要素数とrest項の要素数を計算して結合
→ a_loiの要素数は後者の式の値に1を加えるだけ:
(define (how-many a_loi)
(cond [(empty? a_loi) 0]
[else (+ 1 (how-many
(rest a_loi)))]))
例題2. リストの長さ(要素数)
1. リストが空ならば: → 基底(再帰的繰返しの終了)
解は 0
→ 自明な解(要素がないので個数0)
2. そうでなければ、first部の部分解、 rest部への再帰処理に
よる部分解、これら部分解から最終解の生成を考える:
• リストの rest (これもリスト)の長さに1を足したものが解
...
...
first部は
要素数1
rest 部の要素数を求める(再帰的繰返し)
足すと全体の要素数になる
例題2. リストの長さ(要素数)
No
(empty? a_list)
Yes
(+ 1
(how-many (rest a_list)))
再帰で繰り返し
0 が自明な解である
例題2. リストの長さ(要素数) how-many 関数
値を1つ受け取る(入力)
関数定義を示
a_list の値からリストの
すキーワード 関数の名前
長さを求める(出力)
(define (how-many a_list)
(cond
再帰の停止条件
[(empty? a_list) 0] 基底ケースと解
[else (+ 1
(how-many (rest a_list)))]))
• how-many 内に how-many がある:再帰で繰返す
例: (how-many
(list 11 12))
→ (+ 1 (how-many (list 12)))
まさに「再帰」である
C言語でのhowmany
int howmany(node_t *a_list){
if (a_list == NULL) return 0;
else return 1+howmany(a_list->next);
}
int howmany(node_t *a_list){
int sum=0;
while (a_list != NULL){
sum ++;
a_list = a_list->next;
}
return sum;
}
(how-many (list 11 12)) から
2 が得られる過程の概略
(how-many (list 11 12))
(list 11 12)の長さ
= ...
=(+ 1 (how-many (list 12)))
= ...
(list 12)の長さに1を足す
=(+ 1 (+ 1 (how-many empty)))
= ...
=(+ 1 (+ 1 0))
=(+ 1 1)
=2
empty の長さに1+1を足す
練習2. 数値リストの要素の和
数値リストの要素を合計する関数list-sumを作れ
list-sum
入力
数のリスト
例:(list 1 2 3)
入力はリスト : 任意長の再帰データ
出力
数
例:6
–
テンプレートを使う(how-manyとほぼ同じ)
–
主な違い:cond式の第2節ではfirst項の要素値
とrest項(リスト)に対する部分結果値を合計
例題3. リストが「5」を含むか調べる
•
リストの要素の中に「5」を含むかどうか調
べる関数 contains-5? を作る。
入力は
1つのリスト
contains-5?
(list 3 5 7 9)
•
出力はブール値
(trueかfalse)
true
リスト処理再帰関数のテンプレートを使う
リスト処理の再帰関数の本体定義
まず基底ケース(再帰を含まないcond節)から開始
通常、対応する答えは定式化が容易か、あるいは例により既知。
次に自己参照のケースを扱う。
再帰的な適用 (関数は目的記述文通りに機能すると想定)
→ あとは必要に応じて値を結合することを検討。
結果値の生成(部分的な結果を結合し最終結果へ):
– 多くの場合、結合は基本処理(+ and consなど)で実現
今回– もし first項のデータに対して何らかのチェックが必要なら
入れ子のcond式が必要なことが考えられる
– 場合により補助関数を導入する必要
例題3. リストが「5」を含むか調べる
1. リストが空ならば: → 基底(再帰的繰返しの終了)
解はfalse
→ 自明な解(要素がなければ5もない)
2. そうでなければ、first部の部分解、 rest部への再帰処理に
よる部分解、これら部分解から最終解の生成を考える:
• リストの first が 5 なら true (最終解)。そうでなければ
rest部(これもリスト)を調べた結果が全体の解になる。
...
...
first部
は5か?
そうでなければrest 部を調べる(再帰的繰返し)
ここに場合分けがあるのでリストが非空の場合は条件式
例題3. リストが「5」を含むか調べる
(empty? a_list)
Yes
No
(cond
[(= (first a_list) 5) true]
[else (contains-5? (rest a_list))])
(= (first a_list) 5)
No
Yes
false が解
true が解
再帰
(contains-5?
(rest a_list))
例題3. リストが「5」を含むか調べる
contains-5? 関数
;; contains-5?: list -> true or false
;; it investigates whether 5 is included
;; (contains-5? (list 3 5 7 9)) = true
(define (contains-5? a_list)
(cond
最後まで調べた
自明な解
[(empty? a_list) false]
[else
(cond
探索成功
[(= (first a_list) 5) true]
[else (contains-5? (rest a_list))])]))
未発見→最後まで調べてないので再帰的に探索
→ contains-5? の実行が繰り返される
例: (contains-5? (list 3 5 7 9))
= (contains-5? (list 5 7 9))
C言語でcontains-5?
#define SUCCESS 1
#define FAILURE 0
typedef struct _node{
int data;
struct _node *next;
}node_t;
// contains: node_t * -> int
int contains_5(node_t *p){
while (p != NULL) {
if (p->data == 5)
return SUCCESS;
p=p->next;
}
return FAILURE;
}
// contains: node_t * -> int
int contains_5(node_t *p){
if (p == NULL) return FAILURE;
if (p->data == 5) return SUCCESS;
else return contains(p->next, d);
}
再帰での
今日の課題
課題1.勤務時間リストから賃金リストを生成
• まず時間に対する賃金を求める関数 wage を作る
– 賃金 = 12ドル×勤務時間
• 全従業員の勤務時間のリスト lon から賃金のリスト
を生成する関数 hours->wages を作る
– このとき上記の関数 wage を使う
hours->wages
入力は勤務時
間(数値)のリスト
(list 5 3 6)
出力は賃金
(数値)のリスト
(list 60 36 72)
課題2. 平均点
点数のリストから,平均点を求めるプログラム
average を作り,実行する
– 合計を求める関数 list-sum (練習1)と,リストの長
さを求める関数 how-many (例題2)を組み合わせる
平均点 =
リスト中の点数の総和 / リストの要素数
※ リストの要素数が0の時は,0を返すこと
課題3. シンボル出現の判定
シンボルのリスト los と,シンボル a_symbol を入力と
し, los が a_symbol を含むときに限り true を返す
関数 contains? を作りなさい。
参考:例題3 ただし先頭要素との比較対象は5と決まって
いるわけでなく a_symbolとして与えられる。
contains?
入力はリスト
とシンボル
例:
(list 'hi 'bye) と 'bye
→
(list 'day 'night) と 'noon →
出力はブール値
(trueかfalse)
true
false
課題4. 要素の属性を調べる
リストの要素が 「偶数」かどうか調べる以下の2つ
の関数を作る:
–
ある要素が偶数なら(偶数を1つでも含めば) true.
1つも含まなければ false を返す関数
–
すべての要素が偶数なら true.1つでも偶数でな
いなら false を返す関数
even? を使うこと