2016年度「プログラミング言語」配布資料 (7)
五十嵐 淳 (京都大学大学院情報学研究科 通信情報システム専攻)
2016年12月3日
[ 前の資料 | 講義ホームページ・トップ | 次の資料 ]
2分探索木 in C (C/bst/)
いつものように immutable な2分探索木を実装する.
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
にしているのは,データに,それが何を表すかのデータ(いわばメタデータ)を付加する時に「タグ付け」ということから付けたが,構造体の名前を表す用語の「タグ」と紛らわしかったもしれない.
節点の種類を表すデータは(整数でもよいのだが)列挙型を用いよう.また節点は,葉と枝を表すデータの共用体で定義する.
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
の定義を処理するにあたって,その構造体のサイズを計算する必要がある.しかし,定義の中に,今まさに定義しようとしている型が出てきてしまい,その型のサイズがわからない,といっているのである.
エラーメッセージとしては,型のサイズがわからない,といっているが,実は,このような定義は許しようがない.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))\) で与えることができる.
寄り道
先程も少し触れたように,葉を表すためのデータは何もないので,struct leaf ... lf;
を共用体に含める必要は全くない.さらに,2分木の場合,このメンバを削除すると,共用体のメンバ数が1になってしまい,共用体を使う意味すらなくなるため,
struct tree {
enum nkind { LEAF, BRANCH } tag;
struct branch {
struct tree *left;
int value;
struct tree *right;
} br;
};
という定義でプログラムを作ることも可能であるが,ここでは一般性を考慮して,少し煩雑な定義のまま話を進める.
補助関数
探索などの関数定義に進む前に,branch や leaf を作るための関数を用意しておく.これらは Java でいえばコンストラクタの定義に相当するものと考えられる.branch であれば,左右の部分木(へのポインタ)と格納する整数を引数として,それらを格納した枝(へのポインタ)を返す関数として定義する.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
どちらの関数でも n
を malloc
で確保した領域へのポインタとして使っている.struct tree
は内部に共用体さらには構造体を含んでいるので,どの部分にどのような表記でアクセスするかに注目して定義を読んでもらいたい.この際,
n->dat
は共用体n->dat.br
は共用体を構造体struct branch
として使うためのメンバアクセス- さらにそれに
.left
などをつけることでようやく,部分木(へのポインタ)などにアクセスできる.
探索
さて,ここまできちんと理解できれば2分探索木を操作する関数の定義はさほど難しくはない.以下は find
の定義である.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
2行目で節点の種類によって場合分けを行い,5行目で branch の構造体を取り出し,その値とパラメータ n
の大小によって場合分けを行う.
寄り道(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 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
削除
削除についてもあまりコメントすることがない.min
関数中で,ライブラリの assert
マクロ1が呼ばれているが,これは min
関数は branch
のみに適用されるべきものなので,引数が本当に branch
かどうかを確認している.(万が一 branch
でなかったら,その時点で実行が終了する.)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
|
葉を null ポインタで表現する (C/bstNull/)
Java の時と同様に,2分木の表現のバリエーションとして,何の情報も運ばない leaf
を null pointer で表現する方法を紹介する.null pointer は,何の領域も指さない特殊なポインタである.malloc
で確保した領域も含め,いかなる変数のアドレスとも異なる.既に紹介した通りC言語では null pointer は NULL
という定数で表す.
leaf を NULL
で表すことにすると,そもそも共用体を使う必要がなくなる2ので,型の定義は非常に単純になる.
1 2 3 4 5 |
|
leaf, branch を作る補助関数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
コメントにもあるように,newleaf
は常に NULL
を返す関数である.効率重視のふつうの C プログラマであれば,こんな関数は定義しない(newleaf()
を使うところを NULL
で置き換える)と思うが,最初の実装方法との対応を取りやすくするために,あえて定義している.
残りの関数の定義を一気に示す.与えられた木が leaf か branch か判定するために,null pointer との比較 (t == NULL
) を行っていること,struct tree
の定義が簡単になった分,b
を用意する必要がなくなったことなどに注意すれば,理解できるはずである.(ほとんど Java バージョンと同じであるし,むしろ,こちらの方がプログラムとしてはスッキリしているといってもよいだろう.)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
|
mutable な2分探索木 in C (C/bstMutable/)
次は,mutable な実装である.木構造のデータ定義は(NULL
を使わない)最初と同じであるので,newbranch
,newleaf
なども含めて省略する.また find
は木を書き換えないので,これも定義は同じであるので省略する.
insert
で注意すべき点は,
- leaf を branch に変化させる時には,leaf の領域が不要になるので
free
で解放している(3行目) - branch を表す構造体は局所変数にコピーせずに
&
でポインタを取得している(6行目,上記「寄り道(C上級者への道)」も参照).コピーしてしまうと,元の木構造を書き換えることにならないので,これは必須である.
といったあたりであろう.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
削除についても,不要な領域の解放が注意点だろう(19-21行目,25-26行目,32-33行目).しかも,解放の順番も大事である.先に free(t)
としてしまうと,b
の指す領域(これは t
の領域の途中を指している)も解放されてしまうので,free(b->left);
や free(b->right);
が illegal な操作になってしまう.「解放する時には子供から」が大原則である.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 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;
}
葉を null ポインタで表現する (C/bstMutableNull/)
最後に,mutable でかつ葉を NULL
で表現したバージョンである.おそらく,これがC言語で2分探索木を実装する場合の標準的な実装のひとつだろう.(おそらく一部の関数は再帰を使わずに繰り返しを使って定義すると,よりCらしい.これについては改めて議論したい.)
ほとんどの定義は,これまでの内容が理解できていればそれほど難しくはないが,挿入と削除関連のメモリ管理についてコメントする.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
|
まず,leaf については何の領域も確保されていないので,free
をする必要はない(3行目).また,削除操作で,部分木を残したまま,それをぶら下げている branch を解放する場合には,まず,部分木へのポインタを保存しておいてから,領域の解放,保存しておいたポインタを返している(31-33行目,37-39行目).これを
free(t);
return t->right;
などとすると,解放した領域にアクセスしてまうのでまずい.
[ 前の資料 | 講義ホームページ・トップ | 次の資料 ]
Copyright 五十嵐 淳, 2016, 2017