2018年度「プログラミング言語」配布資料 (12)

Similar documents
PowerPoint プレゼンテーション

第2回

C プログラミング演習 1( 再 ) 2 講義では C プログラミングの基本を学び 演習では やや実践的なプログラミングを通して学ぶ

バイオプログラミング第 1 榊原康文 佐藤健吾 慶應義塾大学理工学部生命情報学科

プログラミングI第10回

Taro-2分探索木Ⅰ(公開版).jtd

PowerPoint Template

memo

Microsoft PowerPoint - algo ppt [互換モード]

RX ファミリ用 C/C++ コンパイラ V.1.00 Release 02 ご使用上のお願い RX ファミリ用 C/C++ コンパイラの使用上の注意事項 4 件を連絡します #pragma option 使用時の 1 または 2 バイトの整数型の関数戻り値に関する注意事項 (RXC#012) 共用

02: 変数と標準入出力

PowerPoint プレゼンテーション

Taro-ポインタ変数Ⅰ(公開版).j

Microsoft Word - 第5回 基本データ構造2(連結リスト).doc

Microsoft PowerPoint - CproNt02.ppt [互換モード]

Taro-スタック(公開版).jtd

02: 変数と標準入出力

(1) プログラムの開始場所はいつでも main( ) メソッドから始まる 順番に実行され add( a,b) が実行される これは メソッドを呼び出す ともいう (2)add( ) メソッドに実行が移る この際 add( ) メソッド呼び出し時の a と b の値がそれぞれ add( ) メソッド

プログラミング実習I

数はファイル内のどの関数からでも参照できるので便利ではありますが 変数の衝突が起こったり ファイル内のどこで値が書き換えられたかわかりづらくなったりなどの欠点があります 複数の関数で変数を共有する時は出来るだけ引数を使うようにし グローバル変数は プログラムの全体の状態を表すものなど最低限のものに留

02: 変数と標準入出力

PowerPoint プレゼンテーション

株式会社アルウィン C 言語コーディング規約 ver.0.1

intra-mart Accel Platform — IM-Repository拡張プログラミングガイド   初版  

alg2015-6r3.ppt

Microsoft Word - no205.docx

gengo1-11

デバッグの工夫

Functional Programming

メソッドのまとめ

PowerPoint プレゼンテーション

COMET II のプログラミング ここでは機械語レベルプログラミングを学びます 1

第 2 章インタフェース定義言語 (IDL) IDL とは 言語や OS に依存しないインタフェース定義を行うためのインタフェース定義言語です CORBA アプリケーションを作成する場合は インタフェースを定義した IDL ファイルを作成する必要があります ここでは IDL の文法や IDL ファイ

Java Scriptプログラミング入門 3.6~ 茨城大学工学部情報工学科 08T4018Y 小幡智裕

JavaプログラミングⅠ

プログラム言語及び演習Ⅲ

データ構造

# let st1 = {name = "Taro Yamada"; id = };; val st1 : student = {name="taro Yamada"; id=123456} { 1 = 1 ;...; n = n } # let string_of_student {n

PowerPoint プレゼンテーション

Microsoft PowerPoint - lec10.ppt

Transcription:

2018 年度 プログラミング言語 配布資料 (12) 五十嵐淳 2019 年 1 月 1 日 目次 1 2 分探索木 in C (C/bst/) 1 1.1 2 分木の表現............................................. 1 1.2 探索.................................................. 4 1.3 挿入.................................................. 5 1.4 削除.................................................. 6 1.5 葉を null ポインタで表現する (C/bstNull/)............................ 7 1.6 短命な 2 分探索木 in C (C/bstMutable/).............................. 10 [ 前の資料 講義ホームページ トップ 次の資料 ] 1 2 分探索木 in C (C/bst/) いつものように永続的な 2 分探索木から実装する. 1.1 2 分木の表現 2 分木の構造の型定義は,Java や C に比べると複雑である. まず, 大枠としては, 節点の種類 ( 葉は枝か ) を表すデータと 節点の情報を表すデータを構造体で組にする. struct tree { T1 tag; // for representing the kind of a node T2 dat; // for representing the node itself } タグ名を tree, メンバを tag, dat とした. これにより struct tree 型の変数 t に対して,t.tag が節点の種類を,t.dat が節点 ( 葉か枝 ) のデータを表す表現となる. 種類を表す部分の名前を tag にしているのは, 1

データに, それが何を表すかのデータ ( いわばメタデータ ) を付加する時に タグ付け ということから付けたが, 構造体や共用体の名前を表す C 言語用語の タグ と紛らわしかったもしれない. 節点の種類を表すデータは ( 整数でもよいのだが ) 列挙型を用いよう. また節点は, 葉と枝を表すデータの共用体で定義する. struct tree { enum nkind { LEAF, BRANCH } tag; union lf_or_br { T3 lf; // for representing a leaf T4 br; // for representing a branch } dat; } enum や union に nkind (node の kind) と lf_or_br (lf または br) という名前をつけたが, これらの型 (enum nkind 型や union lf_or_br 型 ) を持つ変数を使うつもりがないので, これらは実は省略可能である ( 実際, サンプルコードでは lf_or_br は省いた ). ここまでの部分は OCaml で書くと, type tree = Lf of T3 Br of T4 というような定義に対応する.OCaml の場合は Lf は何も情報を運ばないので,of T3 以下は削除,T4 として, レコード型 {left: tree; value: int; right: tree} を使った. ここでは,lf の型 T3 部分は ( どうせ使わないので ) 何でもよいのだが, 統一感から (?) 構造体を入れておくことにする. メンバも与える必要はないが, メンバを持たない構造体は許されていないので ( 試した限りでコンパイラは受理してくれるようだが ), ダミーのメンバを持つ構造体 struct leaf { int dummy; } lf; とした. 枝の方は,3 メンバを持つ構造体になるのだが,OCaml に倣って, struct branch { struct tree left; int value; struct tree right; } br; とすると, コンパイルエラーになってしまう.(MacOSX の clang コマンドでコンパイルした場合は以下のようなメッセージが表示される.) bst.c:16:19: error: field has incomplete type 'struct tree' struct tree left; ^ エラーメッセージがわかりにくいが,incomplete type というのは ( その型の値が占める領域の ) サイズがわからない型のことである. コンパイラは,struct tree の定義を処理するにあたって, その構造体のサイズを計 2

算する必要がある. しかし, 定義の中に, 今まさに定義しようとしている型が出てきてしまい, その型のサイズがわからない, といっているのである. エラーメッセージとしては, 型のサイズがわからない, といっているが, 実は, このような定義は許しようがない. struct tree,enum nkind,struct leaf,int のサイズをそれぞれ s, e, l, i とすると,s = e+max(l, (s+i+s)) を満たす必要がある ( 足し算が構造体,max が共用体のサイズ計算に対応している ) が, これらのサイズは全て正整数なので, こんな s は存在しないわけである. 要するに, ある領域の中に, すっぽりと自分自身をふたつもを含むようなレイアウトをしなければいけないのだが, そんなことは不可能なのである. このような問題を回避して,C 言語で再帰的なデータ構造を扱うためには, ポインタを使うことになる. 以下が最終的な struct tree の定義である. メンバ left, right はポインタになっている. struct tree { enum nkind { LEAF, BRANCH } tag; union { struct leaf { int dummy; } lf; struct branch { struct tree *left; int value; struct tree *right; } br; } dat; // standing for DATum }; このように, 枝は部分木へのポインタをメンバとするように定義する. ポインタであれば, その指す先の領域に関わらずポインタのサイズは一定であるため, 先ほどのサイズを計算する式も,p をポインタのサイズとして,s = e + max(l, (p + i + p)) で与えることができる. 1.1.1 寄り道 先程も少し触れたように, 葉を表すためのデータは何もないので,struct leaf... lf; を共用体に含める必要は全くない. さらに,2 分木の場合, このメンバを削除すると, 共用体のメンバ数が 1 になってしまい, 共用体を使う意味すらなくなるため, struct tree { enum nkind { LEAF, BRANCH } tag; struct branch { struct tree *left; int value; struct tree *right; } br; }; という定義でプログラムを作ることも可能であるが, ここでは一般性を考慮して, 少し煩雑な定義のまま話を進める. 3

1.1.2 補助関数 探索などの関数定義に進む前に,branch や leaf を作るための関数を用意しておく. これらは Java でいえばコ ンストラクタの定義に相当するものと考えられる.branch であれば, 左右の部分木 ( へのポインタ ) と格納す る整数を引数として, それらを格納した枝 ( へのポインタ ) を返す関数として定義する. 1 struct tree *newbranch(struct tree *left, int value, struct tree *right) { 2 // Allocate a new object in the heap 3 struct tree *n = (struct tree *)malloc(sizeof(struct tree)); 4 // And then initialize the members 5 n->tag = BRANCH; // could be written (*n).tag = BRANCH 6 n->dat.br.left = left; 7 n->dat.br.value = value; 8 n->dat.br.right = right; 9 return n; 10 } 11 12 struct tree *newleaf(void) { 13 struct tree *n = (struct tree *)malloc(sizeof(struct tree)); 14 n->tag = LEAF; 15 return n; 16 } どちらの関数でも n を malloc で確保した領域へのポインタとして使っている.struct tree は内部に共用体さらには構造体を含んでいるので, どの部分にどのような表記でアクセスするかに注目して定義を読んでもらいたい. この際, n->dat は共用体 n->dat.br は共用体を構造体 struct branch として使うためのメンバアクセス さらにそれに.left などをつけることでようやく, 部分木 ( へのポインタ ) などにアクセスできる. 1.2 探索 さて, ここまできちんと理解できれば 2 分探索木を操作する関数の定義はさほど難しくはない. 以下は find の定義である. 1 bool find(struct tree *t, int n) { 2 if (t->tag == LEAF) { 3 return false; 4 } else /* t->tag == BRANCH */ { 5 struct branch b = t->dat.br; 6 if (n == b.value) { 7 return true; 8 } else if (n < b.value) { 4

9 return find(b.left, n); 10 } else /* n > b.value */ { 11 return find(b.right, n); 12 } 13 } 14 } 2 行目で節点の種類によって場合分けを行い,5 行目で branch の構造体を取り出し, その値とパラメータ n の大小によって場合分けを行う. 1.2.1 寄り道 (C 上級者への道 ) 5 行目で, struct branch b = t->dat.br; としているが, これだと, 変数 b の領域に構造体全体の内容がコピーされるので, 効率を気にする場合はポインタ操作で済ませるのが C らしいプログラミングである. struct branch *b = &(t->dat.br); // could be &t->dat.br &(t->dat.br) ( この括弧も不要だがわかりやすさのためにつけている ) によって,t の領域の途中 ( 構造体 branch が始まる部分 ) を指すポインタが得られる. もちろんこの場合,b の型が構造体ではなく構造体へのポインタになるので, 以降の木の操作には. ではなく -> を使うよう変更が必要である. struct branch *b = &(t->dat.br); if (n == b->value) { return true; } else if (n < b->value) { return find(b->left, n); } else /* n > b->value */ { return find(b->right, n); } 1.3 挿入 挿入については, あまりコメントすることがない. 1 struct tree *insert(struct tree *t, int n) { 2 if (t->tag == LEAF) { 3 return newbranch(newleaf(), n, newleaf()); 4 } else /* t->tag == BRANCH */ { 5 struct branch b = t->dat.br; 5

6 if (n == b.value) { 7 return t; 8 } else if (n < b.value) { 9 return newbranch(insert(b.left, n), b.value, b.right); 10 } else /* n > b.value */ { 11 return newbranch(b.right, b.value, insert(b.right, n)); 12 } 13 } 14 } 1.4 削除 削除についてもあまりコメントすることがない.min 関数中で, ライブラリの assert マクロ 1 が呼ばれているが, これは min 関数は branch のみに適用されるべきものなので, 引数が本当に branch かどうかを確認している.( 万が一 branch でなかったら, その時点で実行が終了する.) 1 int min(struct tree *t) { 2 assert(t->tag == BRANCH); 3 struct branch b = t->dat.br; 4 if (b.left->tag == LEAF) { 5 return b.value; 6 } else { 7 return min(b.left); 8 } 9 } 10 11 struct tree *delete(struct tree *t, int n) { 12 if (t->tag == LEAF) { 13 return t; 14 } else /* t->tag == BRANCH */ { 15 struct branch b = t->dat.br; 16 if (n == b.value) { 17 if (b.left->tag == LEAF) { 18 if (b.right->tag == LEAF) { 19 return newleaf(); 20 } else /* b.right->tag == BRANCH*/ { 21 return b.right; 22 } 23 } else /* b.left->tag == BRANCH*/ { 24 if (b.right->tag == LEAF) { 1 assert は関数に見えるが, 実は関数ではなく,C 言語のマクロ (macro) と呼ばれる機能を使って定義されている. マクロはいわば略記であって, コンパイラ ( 正確にはプリプロセッサ ) によって定義が展開されて, プログラムの字面上の置き換えが起こる. マクロについては, いずれ講義でも触れたい. 6

25 return b.left; 26 } else /* b.right->tag == BRANCH*/ { 27 int m = min(b.right); 28 struct tree *newright = delete(b.right, m); 29 return newbranch(b.left, m, newright); 30 } 31 } 32 } else if (n < b.value) { 33 struct tree *newleft = delete(b.left, n); 34 return newbranch(newleft, b.value, b.right); 35 } else /* n > b.value */ { 36 struct tree *newright = delete(b.right, n); 37 return newbranch(b.left, b.value, newright); 38 } 39 } 40 } 1.5 葉を null ポインタで表現する (C/bstNull/) Java の時と同様に,2 分木の表現のバリエーションとして, 何の情報も運ばない leaf を null pointer で表現する方法を紹介する.null pointer は, 何の領域も指さない特殊なポインタである.malloc で確保した領域も含め, いかなる変数のアドレスとも異なる. 既に紹介した通り C 言語では null pointer は NULL という定数で表す. leaf を NULL で表すことにすると, そもそも共用体を使う必要がなくなる 2 ので, 型の定義は非常に単純になる. 1 struct tree { 2 struct tree *left; 3 int value; 4 struct tree *right; 5 }; 1.5.1 leaf, branch を作る補助関数 1 struct tree *newbranch(struct tree *left, 2 int value, 3 struct tree *right) { 4 // Allocate a new object in the heap 5 struct tree *n = (struct tree *)malloc(sizeof(struct tree)); 6 // And then initialize the members 2 繰り返すが, これはこのデータ構造の特殊事情である. もしも,leaf, branch 以外にデータを構成する種類があったら共用体を使うこ とになる. 7

7 n->left = left; 8 n->value = value; 9 n->right = right; 10 return n; 11 } 12 13 struct tree *newleaf(void) { 14 /* Real C programmers would avoid defining such a simple function. 15 * It causes overhead of function calls. 16 */ 17 return NULL; 18 } コメントにもあるように,newleaf は常に NULL を返す関数である. 効率重視のふつうの C プログラマであれば, こんな関数は定義しない (newleaf() を使うところを NULL で置き換える ) と思うが, 最初の実装方法との対応を取りやすくするために, あえて定義している. 残りの関数の定義を一気に示す. 与えられた木が leaf か branch か判定するために,null pointer との比較 (t == NULL) を行っていること,struct tree の定義が簡単になった分,b を用意する必要がなくなったことなどに注意すれば, 理解できるはずである.( ほとんど Java バージョンと同じであるし, むしろ, こちらの方がプログラムとしてはスッキリしているといってもよいだろう.) 1 bool find(struct tree *t, int n) { 2 if (t == NULL) { 3 return false; 4 } else /* t is a branch */ { 5 if (n == t->value) { 6 return true; 7 } else if (n < t->value) { 8 return find(t->left, n); 9 } else /* n > t->value */ { 10 return find(t->right, n); 11 } 12 } 13 } 14 15 struct tree *insert(struct tree *t, int n) { 16 if (t == NULL) { 17 return newbranch(newleaf(), n, newleaf()); 18 } else /* t is a branch */ { 19 if (n == t->value) { 20 return t; 21 } else if (n < t->value) { 22 return newbranch(insert(t->left, n), t->value, t->right); 8

23 } else /* n > t->value */ { 24 return newbranch(t->right, t->value, insert(t->right, n)); 25 } 26 } 27 } 28 29 int min(struct tree *t) { 30 assert(t!= NULL); 31 /* t is a branch */ 32 if (t->left == NULL) { 33 return t->value; 34 } else { 35 return min(t->left); 36 } 37 } 38 39 struct tree *delete(struct tree *t, int n) { 40 if (t == NULL) { 41 return t; 42 } else /* t is a branch */ { 43 if (n == t->value) { 44 if (t->left == NULL) { 45 if (t->right == NULL) { 46 return newleaf(); 47 } else /* t->right is a branch */ { 48 return t->right; 49 } 50 } else /* t->left is a branch */ { 51 if (t->right == NULL) { 52 return t->left; 53 } else /* t->right is a branch */ { 54 int m = min(t->right); 55 struct tree *newright = delete(t->right, m); 56 return newbranch(t->left, m, newright); 57 } 58 } 59 } else if (n < t->value) { 60 struct tree *newleft = delete(t->left, n); 61 return newbranch(newleft, t->value, t->right); 62 } else /* n > t->value */ { 63 struct tree *newright = delete(t->right, n); 64 return newbranch(t->left, t->value, newright); 65 } 9

66 } 67 } 1.6 短命な 2 分探索木 in C (C/bstMutable/) 次は, 短命な実装である. 木構造のデータ定義は (NULL を使わない ) 最初と同じであるので,newbranch,newleaf なども含めて省略する. また find は木を書き換えないので, これも定義は同じであるので省略する. insert で注意すべき点は, leaf を branch に変化させる時には,leaf の領域が不要になるので free で解放している (3 行目 ) branch を表す構造体は局所変数にコピーせずに & でポインタを取得している (6 行目, 上記 寄り道 (C 上級者への道 ) も参照). コピーしてしまうと, 元の木構造を書き換えることにならないので, これは必須である. といったあたりであろう. 1 struct tree *insert(struct tree *t, int n) { 2 if (t->tag == LEAF) { 3 free(t); 4 return newbranch(newleaf(), n, newleaf()); 5 } else /* t->tag == BRANCH */ { 6 struct branch *b = &(t->dat.br); 7 if (n == b->value) { 8 return t; 9 } else if (n < b->value) { 10 struct tree *newleft = insert(b->left, n); 11 b->left = newleft; 12 return t; 13 } else /* n > b->value */ { 14 struct tree *newright = insert(b->right, n); 15 b->right = newright; 16 return t; 17 } 18 } 19 } 削除についても, 不要な領域の解放が注意点だろう (19-21 行目,25-26 行目,32-33 行目 ). しかも, 解放の順番も大事である. 先に free(t) としてしまうと,b の指す領域 ( これは t の領域の途中を指しているのであった ) も解放されてしまうので,free(b->left); や free(b->right); が未定義動作を引き起こす不正な操作になってしまう. 解放する時には子供から が大原則である. 1 int min(struct tree *t) { 2 assert(t->tag == BRANCH); 3 struct branch *b = &(t->dat.br); 10

4 if (b->left->tag == LEAF) { 5 return b->value; 6 } else { 7 return min(b->left); 8 } 9 } 10 11 struct tree *delete(struct tree *t, int n) { 12 if (t->tag == LEAF) { 13 return t; 14 } else /* t->tag == BRANCH */ { 15 struct branch *b = &(t->dat.br); 16 if (n == b->value) { 17 if (b->left->tag == LEAF) { 18 if (b->right->tag == LEAF) { 19 free(b->left); 20 free(b->right); 21 free(t); 22 return newleaf(); 23 } else /* b->right->tag == BRANCH*/ { 24 struct tree *right = b->right; 25 free(b->left); 26 free(t); 27 return right; 28 } 29 } else /* b->left->tag == BRANCH*/ { 30 if (b->right->tag == LEAF) { 31 struct tree *left = b->left; 32 free(b->right); 33 free(t); 34 return left; 35 } else /* b->right->tag == BRANCH*/ { 36 int m = min(b->right); 37 struct tree *newright = delete(b->right, m); 38 b->value = m; 39 b->right = newright; 40 return t; 41 } 42 } 43 } else if (n < b->value) { 44 struct tree *newleft = delete(b->left, n); 45 b->left = newleft; 46 return t; 11

47 } else /* n > b->value */ { 48 struct tree *newright = delete(b->right, n); 49 b->left = newright; 50 return t; 51 } 52 } 53 } メモリの解放を本当に気にするなら,main 関数を終了する前に, 確保した領域を全て解放するのが行儀のよいプログラムである. 以下は, 与えられた木の領域を全て解放する関数 free_tree である.main ( ここには掲載していない ) の最後で読んでいる. void free_tree(struct tree *t) { if (t->tag == LEAF) { free(t); } else /* t->tag == BRANCH */ { struct branch *b = &(t->dat.br); free_tree(b->left); free_tree(b->right); free(t); } return; } 1.6.1 葉を null ポインタで表現する (C/bstMutableNull/) 最後に, 短命でかつ葉を NULL で表現したバージョンである. おそらく, これが C 言語で 2 分探索木を実装する場合の標準的な実装のひとつだろう.( おそらく一部の関数は再帰を使わずに繰り返しを使って定義すると, より C らしい. これについては 06-rec-iter.html を参照のこと.) ほとんどの定義は, これまでの内容が理解できていればそれほど難しくはないが, 挿入と削除関連のメモリ管理についてコメントする. 1 struct tree *insert(struct tree *t, int n) { 2 if (t == NULL) { 3 return newbranch(newleaf(), n, newleaf()); 4 } else /* t is a branch */ { 5 if (n == t->value) { 6 return t; 7 } else if (n < t->value) { 8 t->left = insert(t->left, n); 9 return t; 10 } else /* n > t->value */ { 11 t->right = insert(t->right, n); 12

12 return t; 13 } 14 } 15 } 16 17 // The definition of 18 // int min(struct tree *); 19 // omitted 20 21 struct tree *delete(struct tree *t, int n) { 22 if (t == NULL) { 23 return t; 24 } else /* t is a branch */ { 25 if (n == t->value) { 26 if (t->left == NULL) { 27 if (t->right == NULL) { 28 free(t); 29 return newleaf(); 30 } else /* t->right is a branch */ { 31 struct tree *right = t->right; 32 free(t); 33 return right; 34 } 35 } else /* t->left is a branch */ { 36 if (t->right == NULL) { 37 struct tree *left = t->left; 38 free(t); 39 return left; 40 } else /* t->right is a branch */ { 41 int m = min(t->right); 42 t->value = m; 43 t->right = delete(t->right, m); 44 return t; 45 } 46 } 47 } else if (n < t->value) { 48 t->left = delete(t->left, n); 49 return t; 50 } else /* n > t->value */ { 51 t->right = delete(t->right, n); 52 return t; 53 } 54 } 13

55 } まず,leaf については何の領域も確保されていないので,free をする必要はない (3 行目 ). また, 削除操作で, 部分木を残したまま, それをぶら下げている branch を解放する場合には, まず, 部分木へのポインタを保存しておいてから, 領域の解放, 保存しておいたポインタを返している (31-33 行目,37-39 行目 ). これを free(t); return t->right; などとすると, 解放した領域にアクセスすることになってしまう ( 未定義動作 ) のでまずい. [ 前の資料 講義ホームページ トップ 次の資料 ] 14