定本 Cプログラマのためのアルゴリズムとデータ構造 3-2
データ構造の実現
基本データ型
- 例、数値型(整数、文字、浮動小数点数)
基本データ型をベースにして、ポインタ、配列、構造体(stract)、共用体(union)、関数を組み合わせて任意のデータ型をつくれる
- 基本データ型は言語によって大差は無いが、C言語には文字列型がない
列挙型(enumeration type)
あらかじめとり打つ値が限定されている型。
どうするか
マクロを使うという考えを1歩進めればいい。
列挙型は、まさにそれ。
たとえばfood型というudonとriceの2つの値だけとることができる列挙型を作り、
食べ物を表す時に列挙型のfoodを利用するようにすれば、
udonnと書いたときに列挙型foodがとることができない値として書き間違えを検出できる。
enum food{udon, rice, soba} enum food lunch
Cではenumで列挙型をつくる。
上記のようにすると昼飯にはうどんか白米か蕎麦しか食べられないようになる。
ラーメンなどもってのほかである。
配列(array)
同じ型の要素を集めたもの。添え字(index)で識別する。
構造体(structure)
異なる型のデータを集めたもの。C以外の一般ではレコード構造(record structure)と言う。
- メンバ(member)
- 構造体の各要素
- ほかの言語ではフィールド(field)という
ポインタ
- NULLポインタ
- 何もさしていない状態
Cのポインタは3種類くらいに分類できそうだね
- データ構造としてのポインタ
- 参照渡しのパラメタのためのポインタ
- 配列参照のためのポインタ
ふむふむ。
この辺はJavaを使ったことがあるので理解できる。
ポインタについては別途学習しなおす必要がありそうやなー。
定本 Cプログラマのためのアルゴリズムとデータ構造 3-1
今度はデータ構造だ。
用語
- 擬似コーディング
- 擬似プログラム
- 段階的詳細化(stepwise refinement)
- 擬似プログラムを細部をつめながらプログラミング言語に置き換えること
抽象データ型(abstract data type)
データの型とその型に対する一連の操作の組
- 例、「整数の集合」と「和集合をとる操作」の組
抽象データ型に含まれる操作は、別の抽象データ型のデータをパラメタに取れる
抽象データ型の特徴
- 「データの持つべき性質」と「データに施せる操作」を表す
- データの実際の表現方法や、実際の操作については一切規定されていない
カプセル化すると
- 用意された操作を利用しなければデータの参照・変更ができなくなる。
- プログラムの保守開発が楽になる
- 計算量を改善する際に、プログラム全体ではなく抽象データ型の実現部分にのみ手を触れればよくなる。
定本 Cプログラマのためのアルゴリズムとデータ構造 2-2-6
定本 Cプログラマのためのアルゴリズムとデータ構造 2-2-5
ハッシュ法ですね。
hashing。
解決法は?
キーが衝突したときに、同一のハッシュ値をもつデータをポインタでつないだり、(チェインとかいう)
または、完全に衝突しないようなハッシュ関数を生成したり(完全ハッシュ法とかいう)すればいい。
計算コストは?
通常のはハッシュ関数は基本操作だけで実現できるから、O(1)になる。
ハッシュが衝突し、かつデータの個数が多めになると、衝突の解析にコストがかかる。
コストの発生する原因
コストの大半は衝突や衝突の回避によって生まれる。
よってハッシュ関数の質を高めることがコスト削減につながる。
配列の大きさに対してデータの割合が8割をこえると、
ランダム値によるhashingでは衝突を起こし始める。
そうするとコストがO(1)を超えてしまう。
ハッシュ法は時間と空間のトレードオフ
ハッシュ法はデータ格納領域を十分に用意できる場合には、
関数をそれほど工夫しなくても十分な効果を得られる。
また、関数の選択を正しく行えば、登録や探索のコストを
O(1)に収めたり、近づけたりできる。
定本 Cプログラマのためのアルゴリズムとデータ構造 2-2-3
2-2-3はデータ挿入のコストの話。
そうか、検索だけじゃなくて挿入のコストを考えなきゃいけないときも多いよな。
リニアとバイナリのデータ挿入における違い。
リニアはソート不要なので、リストの最後にデータをくっつけるだけ。
一方、バイナリは常にリストがソートされてなきゃ駄目なので、
素朴なアプローチとしては、新しいデータの挿入位置を決めて
挿入位置より後ろのデータを後ろにずらす。
そして、開いた場所に新しいデータを入れることになる。
実装
add_linear(int key, int data){ /*nはデータ個数(データの挿入位置をあらわす配列番号*/ if(n >= TABLE_SIZE){ error("too much data.");} table[n].key = key; table[n].data = data; }
add_binary(int key, int data){ int pos; pos = 2分探索で挿入位置決定 配列のpos以降を1つづつずらす table[pos].key = key; table[pos].data = data; }
n個のデータを追加するための計算量
- バイナリはn回末尾に挿入するだけなのでO(n)
- リニアは、1回の計算に必要な計算量を考えると、挿入位置探索にO(log n)、データずらしは線形時間なのでO(n/2) = O(n)となり、O(max(log n, n, 1))でO(n)も必要になる。さらにこれをn回繰り返すので、合計計算量はO(n^2)となる。
実運用時のことを考える
2分探索法は挿入と検索を同時に行う必要がある場合には向かないことが分かる。
だが、2分探索法は挿入時のコストを軽減することができれば、問題が減りそうだ。
どんなときにコストを減らせる?
最初にすべてのデータをソート可能で、メインでは検索のみの場合
どうやるの?
- 最初はデータをソートせずに配列に挿入する。
- ソートには計算量がO(n log n)のアルゴリズムがあるので、それを使ってソートする
結果は?
結果として挿入にかかるコストがO(n^2)からO(n log n)に激減した。