BFPRT algorithm1. BFPRT algorithm
Blum-Floyd-Pratt-Rivest-Tarjan, 1973
MATSUURA Satoshi
matsuura@is.naist.jp
1
3. 中央値ソート
7 2 8 0 6 3 5
中央値を探し、中央の要素とスワップ
7 2 8 5 6 3 0
中央値より大きい左側の要素を、中央
値以下の右側の要素とスワップ
3 2 0 5 6 7 8
中央値の左右で配列を分割し、
再帰的に部分配列を整列させる
0 2 3 5 6 7 8
2つ、または3つの部分配列で上記処理
が終わると、ソートが完了する。
(1つの部分配列の整列には意味が無い)
再帰的に整列させる部分配列のサイズがほぼ等しくなる(半分に
なっていく)ため、O(n log n)のコストと考えられる。
3
5. partition関数
0 1 2 3 4 5 6
中央値(3番目に小さい数)を
探すことが目的 7 2 8 0 6 3 5
int partition(array, left, right, pivotIndex)
・配列の特定範囲(left, right)をpivot要素を基準に2分割する。
・前半はpivot要素以下の値に、後半はpivot要素より大きな値に分割する。
・pivot要素 = array[pivotIndex]であり、left pivotIndex rightである。
・返値は分割後のpivot要素の位置を表す。
? partition関数があったとしてどのように中央値が算出できるか
? partition関数の実装はどのようなものが考えられるか
5
7. 0 1 2 3 4 5 6
partition(0,6,5)開始
5番目の要素を選ぶ 7 2 8 0 6 3 5
一番右側とスワップ
(一時的な待避) 7 2 8 0 6 5 3
3以下の数を
左側から探す 7 2 8 0 6 5 3
発見できたら
左側からスワップ ② 7 8 0 6 5 3
3以下の数を
継続して探す ② 7 8 0 6 5 3
発見できたら
左側からスワップ ② ⓪ 8 7 6 5 3
3以下の数を探し
終端まで到達 ② ⓪ 8 7 6 5 3
発見できたら
左側からスワップ ② ⓪ ③ 7 6 5 8
0 1 2 3 4 5 6
7
8. 0 1 2 3 4 5 6
partition(0,6,5)
初期状態 7 2 8 0 6 3 5 pivot位置を5番目と選ぶ
partition(0,6,5)の結果
partition実行後 ② ⓪ ③ 7 6 5 8 pivotは2番目の位置
pivot要素以下の数、ここでは pivot要素より大きい数、ここでは
3以下の要素で構成される 3より大きな要素で構成される
② ⓪ ③ 7 6 5 8
0 1 2 3 4 5 6
欲しい数 :3番目に小さな値(中央値)
得られた情報:選択したpivot要素(3)は2番目の位置
pivot要素が3番目の位置であったなら、それが中央値であったのに残念、、、
次は右側の配列の中で0番目に大きな数を探そう。つまりpartition(3,6,0)を実行しよう。
再帰的に追っていけば、いつかは見つかるはず。
8
9. k: 求めるべき中央値の位置(ここではk=3)
p: 得られたpivot位置
0 1 2 3 4 5 6
partition(0,6,6)
初期状態 7 2 8 0 6 3 5 pivot位置を6番目と選ぶ
k = pの場合:
得られたpivot要素が中央値
partition(0,6,6)の結果
partition実行後 ② ⓪ ③ ⑤ 6 8 7 pivotは3番目の位置
0 1 2 3 4 5 6
k > pの場合: partition(0,6,5)
初期状態 7 2 8 0 6 3 5 pivot位置を5番目と選ぶ
[p+1, right]からk-p-1番目に
小さな値が中央値となる。
partition(0,6,5)の結果
[p+1, right]にpartitionを実行。 partition実行後 ② ⓪ ③ 7 6 5 8 pivotは2番目の位置
0 1 2 3 4 5 6
k < pの場合: partition(0,6,0)
初期状態 7 2 8 0 6 3 5 pivot位置を0番目と選ぶ
[left, p-1]からk番目に
小さな値が中央値となる。
partition(0,6,0)の結果
[left, p-1]にpartitionを実行。 partition実行後 ⑤ ② ⓪ ⑥ ③ 7 8 pivotは5番目の位置
9
10. *中央値ソートの手順
1.partition関数を再帰的に実行
2.中央値の発見
3.発見したときにはすでに左側の部分配列は中央値以下に、
右側の部分配列は中央値より大きくなっている
すぐに下位の部分配列の整列に取りかかれる
*partition関数動作例(再掲)
0 1 2 3 4 5 6
partition(0,6,5)
初期状態 7 2 8 0 6 3 5 pivot位置を5番目と選ぶ
partition(0,6,5)の結果
partition実行後 ② ⓪ ③ 7 6 5 8 pivotは2番目の位置
pivot要素以下の数、ここでは pivot要素より大きい数、ここでは
partition実行後はpivotを基準に 3以下の要素で構成される 3より大きな要素で構成される
左側に小さな部分配列が、
右側に大きな部分配列が ② ⓪ ③ 7 6 5 8
出来た事を思いだそう。
0 1 2 3 4 5 6
10
13. partition関数のpivot選択
pivotの選択は両端のいずれかから順に選択する方法、
ランダムに選択する方法など多くの方法が考えられ、
pivot選択方法が全体のパフォーマンスを決定する。
*最悪時のコスト
・pivotの選択を誤ると中央値を探すためにpartition関数を多く実行する必要がある
・partition関数を1回実行した時のコストはO(n)
・最悪時はpartition1回につき一要素しか絞り込めない時であり、n-1回partitionを実行す
る必要がある。つまり一回の中央値を得るコストがおよそn^2となる。一段下位の部分配列
に関してもおよそ2 * (n / 2)^2のコストがかかる。コストの総和は2*n^2を越えないので、
partition全体のコストはO(n^2)。このコストが支配的になり全体でもO(n^2)となる。
なんだか面倒な事になっている。クイックソート使った方が。。。
13
14. pivot選択の失敗に
引きずられて、
O(n^2)になるのは嫌だ。。
それ、BFPRTで出来るよ
14
16. BFPRTアルゴリズムの手順
ソート対象の配列 23 7 9 81 17 11 34 39 44 28 ・・・ 3 72 54
23 11 12 23 11 12
7 34 98 7 39 98
5個の配列に分割し、
9 39 ・・・ 3 17 34 ・・・ 54
それぞれで中央値を求める
81 44 72 81 44 72
17 28 54 9 28 3
部分配列の中央値を集め、
上記の手順を再帰的に繰り返す。
つまり、5個の配列に分割し、
17 34 ・・・ 54 34
それぞれで中央値を求め、
中央値の集合で配列を作る。
またその配列を...と繰り返す
最後に残った要素は真の中
央値に近似した値となり、 partition(array, left, right, 34)
これをpivotして利用する
16
19. 中央値と部分配列の中央値群
ソート対象の配列 23 7 9 81 17 11 34 39 44
ソート済みの状態と真の中央値 7 9 11 17 23 34 39 44 81
ソート済みの配列をA<B<Cとなる ここでは簡単のため
7 9 11 17 23 34 39 44 81
ように3等分にグループ化する 部分配列長を 3 とする
A群 B群 C群
B A A C B A B C C
ソート対象の配列を
23 7 9 81 17 11 34 39 44
部分配列に分割
A B C
部分配列の中央値を求める 9 17 39
B
中央値群から中央値を求める 17
B群以外から最終的な要素が選択される事はあるのだろうか?
19
21. 中央値と部分配列の中央値群
・9個の要素から中央値群を取り出すと最低でも1つB群の要素を含む
→9個の要素からは必ずB群(中央値近辺1/3)の要素が選択される。
(一般化して長い配列を考えてみよう)
・3つの部分配列を選択し、その9個の要素においてA, B, Cの数が等しい場合、
中央値群を選択すると最低でも1つのB群の要素を含む。
・また数列全体において、A, B, Cの数は等しいため、全体の中央値群を選択すると
最低でも中央値群の1/3はB群の要素を含む。
帰納的に、元の配列のB群(中央値近辺1/3)の要素が選択される
A C C B A C A B C A A C B B A B C C A B A B B C A B C
C B B A B C A B B
ABC, ABC, ABCの様な部分配列の組が
出来るとB群が選択されやすくなる
B B B
B
A C C B A A A B C A A C B B A B A A C B C B B C C B C
C A B A B A C B C
意図的にB群が選択されにくいようにしたケース。
中央値群の1/3は常にB群が選択されている。
B A C
B
21