データ構造とアルゴリズム
第四回
(2014年10月10日)
抽象データ型とデータ構造、スタック、キューなど
http://www.sw.it.aoyama.ac.jp/2014/DA/lecture4.html
Martin J. Dürst
© 2009-14 Martin
J. Dürst 青山学院大学
目次
- 前回の残り、まとめ、宿題
- 多項式の重要性
- アルゴリズムの漸近的計算量の求め方
- 漸化式
- 抽象データ型
前回の残りとまとめ
関数の漸近的増加は次の記法で表現可能:
- f(n)∈ O(g(n)):
f(n) は g(n)
の増加以下の関数
- f(n)∈ Ω(g(n)):
f(n) は g(n)
の増加以上の関数
- f(n)∈ Θ(g(n)):
f(n) は g(n)
の増加と同等の関数
f(n)∈O(g(n))
の条件は ∃c>0: ∃n0≥0:
∀n≥n0:
f(n)≤c·g(n) で、
確認は c、n0
の選択や極限の利用で可能
O() 記法では常に一番簡単な書き方を選択
多項式増加と指数的増加の比較
実例:
1.1n ≶ n20
log(1.1)·n ≶ log(n)·20
n/log10(n) ≶ 20/log10(1.1)
≊483.2
n0 ≊ 1541
⇒ a, b > 1
の場合、an の漸近的な増加は
nb
の漸近的な増加より必ず大きい
対数の底
O(log2 n) と O(log10
n) はどう違うか
logb a = logc
a / logc b =
logc a ·
logb c
log10 n = log2 n ·
log10 2 ≅ 0.301 · log2 n
O(log10 n) = O(0.301 ·
log2 n) = O(log2 n)
一般の定数 a と b (a>1,
b>1) の場合、O(loga
n) = O(logb n) =
O(log n)
前回の宿題
(提出不要)
ウェブなどで O(1), O(log n), O(n),
O(n log n), O(n2),
O(n3), O(2n), O(n!)
のアルゴリズムを探す
よくあるオーダー
O(log n) (logarithmic order/time),
O(n log n):
二分探索など、データを小分けして処理する場合
O(n) (linear order, linear time):
データの大きさに比例、全てのデータをチェック
O(n2) (quadratic order/time),
O(n3) (cubic order/time):
二つ、三つのデータの組み合わせを考える場合
O(2n):
データの全ての部分集合を検討する場合など
O(n!):
データの全ての順序を検討するなど
多項式の重要性
- 「有用」といわれる計算量は問題によって違う
- 一般的に:
- 多項式時間 (polynomial time) は有用
- 指数時間 (exponential time) は現実的でない
アルゴリズムの漸近的計算量の求め方
- 入力の大きさの変数 (n など) の決定
- アルゴリズム内の一番多く実行される操作の特定
- 操作の数 (ステップ数) の算出 (総和または漸化式)
- 操作の数から漸近的計算量の算出
- 注: 数の算出の時、既に計算量特有の単純化が可能
入力の大きさの変数の決定
- 多くの場合、データの項目の数 (例: 探索、整列)
- 行列などの場合、幅や高さ (n × n や
n × m の行列)
- 一データ項目の大きさ (例: 非固定長整数の演算)
一番多く実行される操作の特定
- ループ (特に多重ループ) 内の操作が多い
- 複数の独立したループなどの場合、別々に調査
- 操作の数が入力の具体値に依存の場合、
最悪のケースで調査 (最善や平均もあり)
- 操作が関数呼出しの場合、その関数の中身も考慮
操作の数の算出 (総和)
操作の数の算出 (漸化式)
- 具体例:
binsearch(array, low, high, key)
middle = (high+low)/2
if low==high
if array[low]==key
return low
else
return nil
elsif key>array[middle]
return binsearch(array, middle+1, high, key)
else
return binsearch(array, low, middle, key)
- 操作の数を漸化式で表現:
B(n) = B(⌈n/2⌉) + 1
B(1) = 1
漸化式
(recurrence 又は recurrence relation)
- (プログラムの)
再帰的関数と同様に定義されている数学的関数
- 解くには様々な方法が必要
- 繰り返しの置き換えでパターンの発見:
B(n) = B(⌈n/2⌉) + 1 =
B(⌈⌈n/2⌉/2⌉) + 1 + 1 =
B(⌈n/22⌉) + 2 =
= B(⌈n/23⌉) + 3 =
B(⌈n/2k⌉) + k
- B(1) = 1 を利用:
⌈n/2k⌉ = 1 ⇒ 1
≥ n/2k (>1/2) ⇒
2k ≥ n (> 2k-1)
⇒ k ≥ log2 n (> k-1) ⇒
k = ⌈log2 n⌉
- B(n) = 1 + ⌈log2 n⌉
- 漸近的計算量: O(log n)
アルゴリズムの計算量の比較
例: 線形探索と2分探索の比較
2分探索は線形探索より何秒速い?
2分探索は線形探索より何倍速い?
- 2文探索は O(log n), 線形探索は
O(n) のため、2分探索が n
が多くなるにつれ圧倒的に速い
結論: 計算量を O()
記法で表すことで、アルゴリズムそのものの根本的な速さの比較が可能
抽象データ型
(abstract data type, ADT)
- データとそれを操作する関数のセット
- データは関数以外で操作不可能 (カプセル化)
- データの完全性のための概念
例: 生年月日と年齢
- 膨大なソフトウェアのモジュール化のための概念
- 型理論と連結
- オブジェクト指向による実装が多い
- 型 → クラス (class)
- 関数 → メンバ関数 (member function)
又はメソッド (method)
抽象データ型の典型例
- スタック
- キュー
- 線形リスト
- 辞書 (注: 本としての「辞書」と ADT
としての「辞書」は同じではない)
- 順位キュー
スタック
(stack)
- 原理
- last-in-first-out (LIFO)
- 具体例
- 食堂の食膳の山
- IT の例
- プログラムの関数呼び出し用のスタック
- 主なメソッド
- 新規作成 (new), 追加 (push), 削除 (pop)
- その他のメソッド
- 空かどうかのチェック
(empty?)、最上のデータ項目をのぞく (top)
スタックの公理
つぎの四つの公理でスタックの定義が可能
- Stack.new.empty? ↔ true
- s.push(e).empty? ↔ false
- s.push(e).top ↔ e
- s.push(e).pop ↔ s (pop
はデータではなく、スタックを返す場合)
(s が任意のスタック、e が任意のデータ項目)
公理は実装側と使用側の間の約束事
キュー
(queue、待ち行列)
- 原理
- first-in-first-out (FIFO)
- 具体例
- 食堂などの待ち行列
- IT の例
- 実行待ちプロセスのキュー
- 主なメソッド: 追加 (enqueue), 削除 (dequeue)
ADT の比較
実装: 4ADTs.rb
ADT |
スタック |
キュー |
実装 |
Array |
LinearList |
Array |
LinearList |
新規作成 |
O(n) |
O(1) |
O(n) |
O(1) |
項目追加 |
O(1) |
O(1) |
O(1) 又は
O(n)* |
O(1) |
項目削除 |
O(1) |
O(1) |
O(n)* 又は
O(1) |
O(1) |
empty? |
O(1) |
O(1) |
O(1) |
O(1) |
長さ |
O(1) |
O(n) |
O(1) |
O(n) |
*) リングバッファ (ring buffer) により O(1)
に改善可能
まとめ
- アルゴリズムの漸近的計算量は一番多く実行される操作回数から算出
- 算出には総和や漸近式が有効
- O()
記法などでアルゴリズムの根本的性能が表現可能
- 抽象データ型はデータとその操作の組合せ
- 抽象データ型の典型例としてスタック、キューなどが存在
次回のための準備
- 次の関数をオーダの順にならび、その理由を付けなさい。
O(n2), O(n!),
O(n log log n), O(n
log n), O(20n),
- 4ADTs.rb
にあるクラスを使う簡単なプログラムを作成し、実装の性能を比較しなさい。
- 順位キュー (priority queue) という ADT を実装しなさい
(Ruby でも他言語でもよい)
順位キューは各要素ごとに優先度 (整数など)
が付く。一番簡単な場合にデータ項目は優先度だけ。優先度の高いものが先にキューから出る。実装は配列でも連結リストでもその他のデータ構造でもよいよい。