| クイックソート |
ソートアルゴリズムの最後を飾るのは、やはりクリックソートです。
クイックソートは、データの比較と交換回数が非常に少ないのが特徴で、一般的なばらばらデータ(ランダムに散らばっているデータ)に対して、最も効率良く並べ替えを実行します。
クイックソートは、実用上もっとも高速であるとされている並べ替えアルゴリズムで、多くのプログラムで利用されています。
| 考え方 |
ばらばらなデータが格納された配列 a[ ] が与えられた場合に、それらをクイックソートで並べ替える手順を、下の図に示します。

まず始めに、「軸要素」と呼ばれるデータ値を決定します。この値は、データ全体を2つに分割するときのしきい値として使われます。軸要素は、分割が均等に行われるように選ぶのが望ましいのですが、その選択に時間をかけると、かえって並べ替えの時間を大きくしてしまいます。一般には、次のような方法がよく用いられているようです。
上の図では、最後の方法で軸要素を決定しています。
データの先頭から軸要素以上のデータを検索し、データの末尾から軸要素未満のデータを検索し、見つかった場合、それぞれを交換します。この作業を検索ラインが交差するまで続けて、交差した場所でデータを2つに分割します。
分割されたそれぞれのデータに対して同様の処理を繰り返します。
これを再帰的に繰り返していくと、最終的にすべてのデータが小さい順に並び替わります。
Java で書かれたクイックソートアルゴリズムを以下に示します。
/* * 軸要素の選択 * 順に見て、最初に見つかった異なる2つの要素のうち、 * 大きいほうの番号を返します。 * 全部同じ要素の場合は -1 を返します。 */ int pivot(int[] a,int i,int j){ int k=i+1; while(k<=j && a[i]==a[k]) k++; if(k>j) return -1; if(a[i]>=a[k]) return i; return k; } /* * パーティション分割 * a[i]〜a[j]の間で、x を軸として分割します。 * x より小さい要素は前に、大きい要素はうしろに来ます。 * 大きい要素の開始番号を返します。 */ int partition(int[] a,int i,int j,int x){ int l=i,r=j; // 検索が交差するまで繰り返します while(l<=r){ // 軸要素以上のデータを探します while(l<=j && a[l]<x) l++; // 軸要素未満のデータを探します while(r>=i && a[r]>=x) r--; if(l>r) break; int t=a[l]; a[l]=a[r]; a[r]=t; l++; r--; } return l; } /* * クイックソート(再帰用) * 配列aの、a[i]からa[j]を並べ替えます。 */ public void quickSort(int[] a,int i,int j){ if(i==j) return; int p=pivot(a,i,j); if(p!=-1){ int k=partition(a,i,j,a[p]); quickSort(a,i,k-1); quickSort(a,k,j); } } /* * ソート */ public void sort(int[] a){ quickSort(a,0,a.length-1); }
このアルゴリズムは、軸要素の決定に上記の 4. の方法を使用しています。
メソッド pivot() は、軸要素となるデータの番号を返します。もし、すべてのデータが同じ値を持つ場合、それ以上の分割は必要ないので -1 を返します。
メソッド partition() は、pivot() で得られた軸要素を使用してデータを2つのブロックに分割します。このメソッドは、分割した点の番号を返します。
そして、メソッド quickSort() は、与えられた配列 a[ ] の a[i]〜a[j] の範囲を並べ替えます。データを分割する毎に、再帰的にこのメソッドを呼び出すことで、データの並べ替えを行っています。
| クイックソートの動作 |
クイックソートを可視化して行うアプレットを、下に示します。
これを見ると、まず全体が大きく上下に分割されて並べ替わり、さらに上側が2つに分かれて並べ替わり…という手順が繰り返されることがわかります。
| 計算時間の評価 |
クイックソートアルゴリズムでは、データを分割しながら、
の2つの処理を繰り返します。したがって、まず、これら2つの処理に要する時間量を求めます。
pivot(a,i,j) は、配列 a[ ] の i 番目から j 番目までの要素の中から、軸要素として使用するデータの番号を検索します。この処理は、i から j までを順番に検索しますから、検索する要素数分の O(j - i +1) の時間を要します。
partition(a,i,j,x) は、配列 a[ ] の i 番目から j 番目までの範囲を、前後から検索して交差するまで x を軸としてデータの入れ替えを行いますから、やはり、O(j - i +1) の時間を要します。
したがって、これらの処理にはどちらもデータ数に比例した時間がかかることがわかります。
データの分割がもっとも効率よく行われると、各データは2分割ずつされていきますから、クイックソートの分割の深さは、データ数を N をすると log(N) になります。このとき、各深さのデータ数はすべて N ですから、この場合のクイックソートの総時間量は O(N log(N)) になります。
しかし、分割がアンバランスになる場合もあります。最悪の場合は、分割が 1:N-1 に行われつづけた場合で、この場合、クイックソートの分割の深さは N となり、それぞれのデータの深さは N , N , N-1 , N-2 , … , 3 , 2 となります。したがって、この場合のクイックソートの総時間量は、
N + N + N-1 + N-2 + … + 3 + 2 = N2/2 + 3N/2 -1
となり、オーダーは O(N2) となります。
また、平均的な場合として、すべて異なる一様分布のデータに対しては、O(N log(N)) になることが知られています。