基礎勉強会 #5

データ構造

今回の話

  • データ構造とは何か?
  • 計算量
  • 基礎的なデータ構造
  • 参考文献

データ構造とは何か?

データ構造とは何か?

データの集まりをコード・アルゴリズム上で扱いやすくするため、API やメモリ上の配置方法をまとめたもの

 

  • 実際のコードでは抽象データ型を通してアクセスするため、実装を意識する機会は少ないかも
    • API だけ知っていれば十分?

データ構造はなぜ重要か?

「アルゴリズム + データ構造 = プログラム」

 ちょっと古い主張だが…

 

  • プログラムの対象をうまく抽象化するとよい実装になる
    • かも
  • データ構造の選択は性能に大きく影響する
    • API はそのまま、実装だけ入れ替えることもできる
    • 実装を入れ替えるだけで大きく性能向上する場合も

データ構造いろいろ

  • データ構造
    • 「データ構造」とだけいう場合はシーケンシャルなアクセスを前提にしていることが多い
  • 並行データ構造
    • 並行に操作できるデータ構造
    • 大抵はデータ構造にロックを加えたもの
  • 分散データ構造
    • 複数マシンから扱えるデータ構造
  • インメモリ vs オンディスク
    • 今回はインメモリだけ
    • オンディスクになると、また違った評価指標

計算量

アルゴリズムの良し悪しをどう判断するか?

  • 実際の実行時間を使う?
    • メモリ、CPU など環境によって違う
  • 入力の大きさによってどのように変化するかを知りたい
    • → 計算量

計算量

次のような計算を考える


for (i = 0; i < n; i ++) {
     for (j = 0; j < n; j++) {
         c[i][j] = a[i] * b[j];
    }
}
T(n) = 2 m_{read} n^2 + m_{wirte} n ^ 2 + 4 c n ^ 2
T(n)=2mreadn2+mwirten2+4cn2T(n) = 2 m_{read} n^2 + m_{wirte} n ^ 2 + 4 c n ^ 2
T(n) = O(n^2)
T(n)=O(n2)T(n) = O(n^2)
  • メモリアクセス m や 演算 c は環境依存
  • 入力の大きさによって変化するのは n の部分だけ

これをビッグオー記法にすると、

ビッグオー記法

  • 入力サイズ n に対する実行時間を O(f(n)) で表す
    • f(n) - 増加率
O(n^2) + O(n) = O(n^2)
O(n2)+O(n)=O(n2)O(n^2) + O(n) = O(n^2)

大きい項だけ残す

O(log n) < O(n) < O(n log n)
O(logn)&lt;O(n)&lt;O(nlogn)O(log n) &lt; O(n) &lt; O(n log n)

大きければ性能が悪い

比較

計算量の例

計算量
O(1) ArrayList から値の取り出し
O(log N) 二分木の探索
O(N)  単純なループ
O(N log N) クイックソート
O(N ^ 2) 2重ループ
O(N!) N 個すべての組み合わせの列挙

データ構造の性能

  • 正しさ
    • 今回は触れない
  • 時間計算量 (time complexity)
    • データ構造の操作にどの程度の時間がかかるか?
  • 空間計算量 (space complexity)
    • データ構造がどれだけのメモリ領域を使うか?

基礎的なデータ構造

  • リスト
  • ハッシュテーブル
  • ツリー

リスト

リスト

  • 線形シーケンスなどとも
  • 固定長の場合は vector ということもある
    • or tuple , pair など言語によって扱いは異なる
    • array は「配列」を指すことが多い
  • Queue,  Stack, Deque の実装であることも多い

リストの API

size() リストの長さ n を返す
get(i)  i 番目の要素を返す
set(i, x)  i 番目の要素を x にする
add(i, x)  x を i 番目に追加し、以降を後ろにずらす
remove(i)  i 番目の要素を削除し、以降を前にずらす

配列 - Array

  • 固定長
  • インデックスによるランダムアクセスが基本
  • C 言語
    • NULL終端でより短い長さを表現することもある
    • もちろん非効率

backing array or array list

  • 配列をリストとして使用
  • int length を持つ
  • 先頭は固定とする(説明のため)

a

c

d

backing array に対する操作の実行時間

size() O(1)
get(i) O(1)
set(i, x) O(1)
add(i, x) O(n-i)
remove(i) O(n-i)
  • n はリストの長さ

backing array の add

  • メモリアロケーションと n 回のコピー
  • 場合によっては resize

a

b

c

d

a

b

c

c

d

a

b

e

c

d

i 以降のコピー

i への代入

backing array の add

a

b

c

d

a

b

e

c

d

元の配列からコピー

i への代入

d

c

a

b

新しい配列の生成

d

c

a

b

resize あり

Linked List

単方向連結リスト

  • リストを n 個のノードで表現する
  • 各ノードは次のノードへの参照を持つ

Linked List

双方向連結リスト

  • リストを n 個のノードで表現する
  • 各ノードは前後のノードへの参照を持つ

双方向連結リストに対する操作の実行時間

size() O(1)
get(i) O(min{i, n-i})
set(i, x) O(min{i, n-i})
add(i, x) O(min{i, n-i})
remove(i) O(min{i, n-i})
  • size() についてはリストの長さを持っている前提
  • 終端付近へのアクセスは早い
  • 中央部へのアクセスは遅い

他のリスト

  • スキップリスト
    • get, set, add, remove が全て O(log n)

ハッシュテーブル

ハッシュテーブル

  • よく使われるデータ構造
  • 広範囲の値を取りうるオブジェクトを少数格納するのに効率がよいデータ構造
    • HashMap (Java)
    • dictionary (python)

ハッシュテーブルの API

add(x) 要素を追加
find(x) 要素があるかどうか確認する
get(x) 対応する要素を取得
remove(x) 要素を削除

ハッシュテーブル

オブジェクトのハッシュ値を計算し対応する場所に入れる

  • ハッシュ値 ( hash(x) ) によっては性能が大きく変わる
array<TableCell> t;
int n;

bool add(T x) {
  if (find(x) != null) return false;
  if (n+1 > t.length) resize();
  t[hash(x)].add(x);
  n++;
  return true;
}

ハッシュ値衝突時の処理

ハッシュ値が衝突した場合どうするか?

  • チェイン法
  • オープンアドレス法

ハッシュテーブル チェイン法

  • 同じハッシュ値を持つオブジェクトはリストに入れる
  • ハッシュ値のとり方が悪いと単なるリストになってしまう

ハッシュテーブル オープンアドレス法

hash(x) となる要素が既にあった場合には、テーブルを線形に探索 = 1 つずつずらしながら空いている場所を探す

ハッシュテーブルに対する操作の実行時間

find(x) O(1)
get(x) O(1)
remove(x) O(1)

かなり理想的な想定

  • これがうまくいくかは実装による
  • ハッシュ値は衝突しないようにしたい

ツリー

木 - ツリー

  • 様々な形で使われているデータ構造
  • 例)
    • DOM
    • 抽象構文木 (Abstract Data Tree)

木の要素

深さ

(depth)

(leaf)

(root)

(parent)

(child)

二分木

  • 全ての子が 2 個以下

木の幅優先探索

  • 深さを増加させつつ、同じ深さのノードを探索する
  • 条件に合うもののうち、より浅い深さのノードを発見できる
  • 探索中の深さのノード全てを覚えておく必要がある

木の深さ優先探索

  • 各ノードの子を優先して辿り探索する
    • 行きがけ順、通りがけ順、帰りがけ順
  • 辿ってきたノードだけを覚えておけば良い
  • 浅い深さのノードが見つかる保証はない

二分探索木

  • 左の子孫の値 ≦ ノードの値 ≦ 右の子孫の値
  • ソート済み集合として扱える
    • 深さ優先・帰りがけ順でノードを辿れば良い

5

10

8

二分探索木に対する操作の実行時間

  • 平衡状態 (葉の深さが同程度) を仮定している
  • その場合、二分木の深さは log n
find(x) O(log n)
get(x) O(log n)
remove(x) O(log n)

平衡状態でないと…

  • 最悪の場合は Linked List と同じ構造になってしまう
    • この場合は get, find, remove も O (n)

8

4

1

その他のツリーの種類

  • 2-4 木
  • スケープゴート木
  • 赤黒木
    • get などが最悪でも O(log n) になる
    • 実装は難しい
  • ヒープ

など

参考文献

参考文献

  • データ構造とアルゴリズム(情報処理シリーズ)
    • いい本だがかなり内容が古いかも
  • みんなのデータ構造
    • 最近出た本
    • これから買うならこっち

次回の候補

  • アルゴリズム(次回)

  • ネットワーク

  • 仮想化・コンテナ
  • RDBMS, NoSQL
  • オブジェクト指向プログラミング
  • 関数型プログラミング
  • 継続的インテグレーション
  • 言語処理系
  • 文章術
  • 量子コンピューティング

データ構造

By Shingo Suzuki

データ構造

データ構造の話

  • 1,038