quick sort
クイックソート

レポート提出確認

この授業のトップページから レポートのページに移動し, レポートの提出状況を常に確認せよ.

講義

先週まで学習した,簡単だがO(n2)の「遅い」整列アルゴリズムの バブルソート,選択ソート,挿入ソートの要素数5万,10万のときの 実行デモを授業と並行して行う.この中で一番高速な選択ソートを要素数80万で実行し, 計算時間がオーダーの通り,入力の2乗に比例すること確認する.

次に同じ要素数 80万でクイックソートを実行して実行速度の違いを体感する.

14 クイックソート

14.1

14.1.1 クイックソートの考え方

バブルソートなどの単純なアルゴリズムが低速なのは, 近くの要素同士の入れ替えしか行わないためである. 隣合う要素を比較・交換の対象としている限り整列アルゴリズム の計算量は O(n2) の壁を超えられない(p.194 参照).

シェルソートではあらかじめ離れた要素を入れ替えておくという 戦略で高速化されている.クイックソートも離れた要素の交換を 効率良く行うことを狙ったアルゴリズムである.

p.203の説明ではまず a[v] に枢軸を移動して, その両側に小さな要素と大きな要素を振り分ける, とあるが,最初に枢軸の値を決めた時点では 枢軸の左に最終的に振り分ける個数が決まっていないので v の値(枢軸の位置)を決めることは出来ず, この順番では処理できない.

この教科書では実際には,

  1. 枢軸のを決める
  2. 枢軸より小さい値を左に,大きい値を右に集める. 枢軸と等しい値はどちらでもよい.
  3. 枢軸の値を適当な場所に置く(そこよりも左には 枢軸より大きい値がなく,右には小さい値がない場所)

という手順で実装している.

クイックソートも分割統治法で実現できる. 枢軸の左右の部分配列の問題に分割し, 再帰呼出しで処理すればよい.

p.204 Fig.14.2 の(2)にも上で述べたことと同様の 問題がある.実際にa[v]よりも小さい要素を集め終わらないと v の値が決まらないはずである. (2)は上述の 1.2.3. のように書き換えたほうが 実装方法にあっている.書き換えたクイックソートの疑似コードは 以下のようになる.

quick_sort(int a[], int l, int r)
{
    if (整列する要素が 1 個のみである)
        return;
    整列する要素の値の一つを変数 pivot に代入し,       /*   */
    それ以外でpivot より小さい要素を左(左端はa[l])に,/*(2)*/
    pivot より大きい要素を右(右端はa[r])に集め,      /*   */
    pivot の値を左右の間に置く(a[v]とする)            /*   */
    quick_sort(a, l, v - 1);
    quick_sort(a, v + 1, r);
}

図11.1. quick_sort の疑似コード(修正版)

クイックソートでは,配列の一部に対するソートが実行できなければ ならない.quick_sort(int a[], int l, int r) で a[l] から a[r] までの要素がソートできるように 関数 quick_sort を作成する.

quick_sort の疑似コードについても再帰の3つのポイント

  1. 引数のデータが小さいときは再帰呼出しをせず,直接処理する.
  2. 再帰呼出しを行うときは自分が受け取った入力データより 小さいデータを引数として再帰呼出しを行う.
  3. その再帰呼出しの部分は正しく処理される,という前提で 残りのプログラムを書く.

を満たしていることがチェックできる.

図11.1 の疑似コードの(2)の部分がクイックソートの仕事のほとんどを占める. この処理を分割(partitioning)と呼ぶ.

分割のアルゴリズム

ある値がソート後に置かれるべき位置のことを「本来の」位置と 呼ぶと,クイックソートの分割では本来の位置よりも極端に左にある 大きな要素と本来の位置よりも極端に右にある小さな要素を 入れ替えることが戦略の中心になる.この距離の大きな入れ替えで 高速な整列を可能にしている.

アルゴリズムはp.206のFig14.3がわかりやすい. ここでは枢軸として右端 a[r] の値(30)を使う. このため,分割対象の左端が a[l],右端は a[r-1] となる.分割は要素数が 2 以上の時しか行わないので r-1 が l よりも小さくなることは無い.

左端(l)からスタートして右へ移動するポインタ添字 i と 右端(r-1)からスタートして左へ移動するポインタ添字 j と を使い,a[i],a[j] を枢軸と比較する. 両方が入れ替えるべき数値がある場所まで進め, a[i]とa[j]を交換する.

これを i と j がぶつかるまで行う.

p.207 2 から 4 行目の 「ポインタiの左側には30よりも小さい要素のみが,ポインタjの右側には 30よりも大きい要素のみが」の部分は後のList 14.1 の実装では 「ポインタiの左側には30以下の要素のみが,ポインタjの右側には 30以上の要素のみが」となっている.枢軸と同じ値が あった場合のためである.クイックソートのアルゴリズムは 同じ値が複数あった場合の理解が難しいので 最初は Fig. 14.3. のように同じ値が存在しない データで考えると良い.

14.2 クイックソートのプログラム

List 14.1 が図11.1. の疑似コードを実現したものである. 分割処理はそれだけで複雑な処理なので,(Fig. 14.3 の処理を行う) partition という名前で関数として独立させてある. クイックソートのプログラムも他の整列プログラムとは違い, 配列,左端,右端を引数として与える処理として書かれる. これを quick_sort_1 として作成し, 他の整列と同じように引数を配列,要素数として呼び出せるように インターフェースを統一するための関数 quick_sort を作成している.

List 14.1 の 7,8 行目で左から挟むポインタ添字 i の値の初期値が l-1, 右から挟むポインタ添字 j の初期値が r でスタートしており, どちらも本来のスタート位置よりも一個前になっている. (i は l からスタートして右に進む.j は a[r] に枢軸があるため, その左の r - 1 からスタートして左に進む) これは i は 14 行目の ++i で前置インクリメントされるので 最初に使われる値が 1 プラスしたものになるためである. (++i は値を使う前にインクリメントする) 同様に j は 17 行目の --j でデクリメントされてからa[j]にアクセスするので 最初に使われる値は 1 減らされたものになるので正しく動作する.

練習11.1

p.208 List14.1 を動作させよ. 練習10.1 と同様の追加を行うことで動作させることができる. (関数 print_array も同時に追加せよ) 但し,プロトタイプ宣言を書く部分(#include <stdio.h> より後,関数定義より前)に 関数 partition, print_array, quick_sort, quick_sort_1 のプロトタイプ宣言を書かなければいけない. それぞれの関数定義の書き出しを参考に正しく記述せよ. また,main でソート関数を呼び出すときの 関数名を quick_sort に変更することも必要である.

ループが行き過ぎないように必ず条件式にかかってループが止まるような 十分大きい値,あるいは十分小さい値を端に入れておく手法を 番兵(sentinel)と言う.

14.3 クイックソートの計算量

クイックソートがオーダーだけでなく, 実用の場面で高速なのは,pivot の値をレジスタに 入れるなど,計算機の設計に向いた高速化が行える ことなどが理由である.

クイックソートの実行時間にはばらつきがある. 枢軸として選ばれる値が配列を2等分する分割が 行えるような値(中央値)であれば理想的だが, 枢軸の値は制御できない.最悪なのは枢軸の 片方に全てのデータが行く場合で,整列済みの 配列が入力として与えられたときににこの ケースになる.このときのオーダーは O(n2) となる.また,このときの再帰呼出しの深さも 最悪になり O(n) となる.

14.4 クイックソートの改善

スタックが深くなりすぎて処理が不可能になる, という状況を回避する方法として,

  1. 再帰呼出しを行わないプログラムにする
  2. 再帰呼出しの回数が減るように工夫する

ことが考えられる.さらに 2. は以下の 2つのレベルで対処が考えられる.

  1. 中央値はもとのList 14.1. の改善前の プログラムと同じなので分割の偏りは同じだが 短い方から処理を行う.
  2. 中央値を工夫して 左右が偏らない分割ができるようにする.

この 2 は抜本的な改善が期待できる.具体的には 左端と中央と右端の3つの数値を調べ, 中間の順位の値を枢軸として使う実装が よく用いられる.

配列の長さが 1 になるまでクイックソートを 行うのではなく,ある程度小さくなったら 挿入ソートに切り替えるように実装する方法 がある.

課題

第 11 回課題「合計の計算」 を提出せよ.


Updated in December 3, 2012, index.html, Yamamoto Hiroshi