データ構造とアルゴリズム
第六回
(2012年11月9日)
クイックソート、平均計算量
http://www.sw.it.aoyama.ac.jp/2012/DA/lecture6.html
Martin J. Dürst
duerst@it.aoyama.ac.jp
© 2008-12 Martin
J. Dürst 青山学院大学
目次
- レポートについて
- クイックソート: 概念、実装、効率化
- 平均計算量
- O(n log n) より早い整列法
- C や Ruby での整列関数
前回のまとめ
- 整列は情報テクノロジーで重要
- 単純な整列法は全て O(n2)
- 二項目の比較に基づく整列法は O(n log
n)
- マージソートのメモリ量はデータの二倍
- ヒープソートは比較、交換が多い
レポートについて
[諸事情により削除]
今日の目的
- アルゴリズムの概念から実用までの開発
- 平均の計算量
- アニメーションによるアルゴリズムの勉強
- O(n log n) より速い整列
クイックソートの歴史
(quicksort)
- 1960 年に C. A. R. Hoare が発明
- それ以来、徹底的に研究
- 幅広く応用
分割統治法の見直し
- ヒープソート: 大きい山の最大値は二つの
小さい山の最大値
- マージソート:
データを二つの部分に分割、部分を整列したのちに併合
- クイックソート:
データをある分割要素を境に分割したのち、それぞれの部分を整列
クイックソートの基本動作
- ソートされる要素を一つ選択
分割要素 (partitioning element や pivot) とよぶ
- 要素の交換で
分割要素より小さい要素を左に、
分割要素より大きい要素を右に配置
- 分割要素の左と右の部分にクイックソートを再帰的に適用
Ruby による実装: 6qsort.rb の
conceptual_quick_sort
クイックソートの実装
- 最右の要素が分割要素
- 右から分割要素より小さい要素を検索
- 左から分割要素より大きい要素を検索
- 2. と 3. で検索した要素を交換
- 交換の必要がなくなるまで 2.-4. を繰返す
- 分割要素を分岐点に置く
- 左と右で再帰的に実行
Ruby による実装: 6qsort.rb の
simple_quick_sort
最悪時の計算量
(worst case running time)
- 最悪の場合は最大 (又は最小) の要素を
分割要素に選択
- 計算量が Qw(n) = n +
Qw(n-1) = Σni=1
i
⇒ O(n2)
- 既に整列済み (又は逆整列済み) のデータで
実際に最悪になる
最善時の計算量
(best case running time)
- QB(n) = n + 1 + 2
QB(n/2)
⇒ O(n log
n)
- 参考になるか不明
平均の計算量
(average running time)
- 前提: 入力の全ての置換は同等の確率
- QA(n) = n + 1 + 1/n
Σ1≤k≤n
(QA(k-1)+QA(n-k))
ただし、n < 2 の場合 QA(n) =
0
QA の計算
QA(n) = n + 1 + 1/n
Σ1≤k≤n
(QA(k-1)+QA(n-k))
QA(0) + ... + QA(n-2) +
QA(n-1) =
= QA(n-1) + QA(n-2) + ... +
QA(0)
QA(n) = n + 1 + 2/n
Σ1≤k≤n
QA(k-1)
n QA(n) = n (n + 1) +
2 Σ1≤k≤n
QA(k-1)
(n-1) QA(n-1) = (n-1)
n + 2
Σ1≤k≤n-1
QA(k-1)
QA の計算 (続き)
n QA(n) - (n-1)
QA(n-1) = n (n+1) -
(n-1) n + 2 QA(n-1)
n QA(n) = (n+1)
QA(n-1) + 2n
QA(n)/(n+1) =
= QA(n-1)/n + 2/(n + 1)
=
= QA(n-2)/(n-1) + 2/n +
2/(n+1) =
= QA(2)/3 + Σ3≤k≤n
2/(k+1)
QA(n)/(n+1) ≈ 2
Σ1≤k≤n 2/k ≈
2∫1n 1/x
dx = 2 ln n
QA の計算結果
QA(n) ≈ 2n ln n ≈ 1.39
n log2 n
⇒ O(n log n)
⇒ 比較の数が最善の決定木に比べ平均で約 1.39 倍
分割要素の選択
- クイックソートの効率が分割要素の選択に強く依存
- 乱数で選択 (ランダム化アルゴリズム
(randomized algorithm) の例)
- 三項目の中央値 (median of three)
クイックソートの諸問題
- 指数の比較
→番兵で不要
- スタックの深さ
→分割の小さい部分を再帰で優先、大きい部分を末尾再帰や繰り返しで処理
- 小さい部分の整列の効率
→一定の大きさ以下の場合、単純な整列法に切り替え
→挿入整列法の場合、最後に一括でも可能
(テストで要注意)
→10 項目前後で切り替えると約 10%改良可能
- キーの重複
→三分割
Ruby による実装 (三分割以外): 6qsort.rb の quick_sort
アニメーションによる整列法の比較
アニメーションの閲覧: sort.svg
安定な整列法
- 定義: 値が同等の項目の順番を保持する整列法
- 複数項目の整列の場合に便利
- 優先度の低い項目から整列
- 優先度の高い項目を安定に整列
- ヒープソートとクイックソートは安定でない
→複数の項目を同時に整列
→元の順番を優先度の低い整列項目に
O(n log n) より早い整列法
- 今までの整列法は値の任意な分布に対応
- 比較の決定木で最低 O(n log n)
- 値の分布について前知識があると改善可能
- 極端な例: 1 から n までの数の整理
→最終的な場所が完全に予想可能
→O(n)
- 基数整列 (radix sort)、
ビンソート (bin sort, bucket sort) など
ビンソート
例: 学生番号で整列
- 最上位の桁で 10 の山に分割
- 各々の山を再帰的に順に下位の桁で分割
- 山の大きさに対応するため、分割を二つのフェーズで実行
- 各々の山の大きさを計算
- 要素の再配置
- 計算量は O(n
k) (b はビンの数、k
は桁の数)
- 基数整列は最下位の桁から;
上位の桁で分割する必要がない
C や Ruby での整列
- 整列はライブラリ関数やメソッドとして用意
- クイックソートによる実装が多い
- 項目の比較はデータの種類や整列の目的に依存
→比較関数を引数に
- 比較に時間がかかる
→整列に使われる値を事前計算
- データの入れ換えに時間がかかる
→参照だけ整列
C 言語の qsort
関数
void qsort(
void *base, // 配列のスタート
size_t nel, // 配列の要素数
size_t width, // 要素の大きさ
int (*compar)( // 比較関数
const void *,
const void *)
);
Ruby の Array#sort
(Klass#method
: クラス Klass
のインスタンスメソッド method
)
array.sort
: <=>
演算子による整列
array.sort { |a, b| a.length <=> b.length }
{} 内はブロック (block) (「比較関数」)
(例えば文字列の) 長さで整列
Ruby の <=>
演算子
a と b の値の関係 |
a <=> b の戻り値 |
a < b |
-1 |
a = b |
0 |
a > b |
+1 |
Ruby の Array#sort_by
array.sort_by { |a| a.length }
(例えば文字列の) 長さで整列
配列の項目ごとに値を事前計算
次回のための準備
conceptual_quick_sort
が失敗する入力を考える
- アニメーションをよく観察し、整列法の知識を深める