概要
クイックソート(quick sort)は、 名前に quick なんて単語を入れるだけあって、 大半の状況下で最速となるソートアルゴリズムです。 「不安定」な「内部」ソート。
いわゆる、分割統治法的な考え方に基づいて、 大まかにソート → 配列を2つに分割という処理を再帰的に繰り返します。
-
配列の中からある適当な数(pivot: 中心、軸。枢軸と訳す。)を選ぶ。
-
配列を左右両端から見ていって、左側では枢軸よりも値の大きい物を、右側では枢軸よりも小さい物を探す。
-
2 で探した左の値と右の値を入れ替える。
-
3 を繰り返し適用し終えた時点で、配列の左側には枢軸以下の要素が、右側にはそれ以上の要素が集まる。
-
左側の部分と右側の部分に対して、再帰的に同様の処理を繰り返す。
平均計算量的には O(n log n) で、 他の O(n log n) ソートアルゴリズムに比べてもかなり高速ですが、 ワーストケースでは計算量が O(n2) になってしまうという欠点もあります。 枢軸要素の選び方次第では、 ソート済みの配列に対してクイックソートアルゴリズムを適用するという分かりやすい状況下でワーストケースに陥ってしまいます。
しかしながら、長年の歴史を経て、 ワーストケースに陥らないための問題回避策も研究されていて、 ほぼどんなデータに対しても O(n log n) に近い性能が発揮できるような改良版が考案されています。
また、分割がある程度短くなったときに、「挿入ソート」(O(n2) だけど、短い配列に対してはきわめて高速)に切り替えるという手法により、更なる高速化が図れます。 このような様々な工夫を施すことで、 安定性を気にする必要のない場面においては最高速のソートが得られます。
サンプルソース
https://github.com/ufcpp/UfcppSample/blob/master/Chapters/Algorithm/Sort/QuickSort.cs
/// <summary>
/// クイックソート。
/// </summary>
/// <param name="a">対象の配列</param>
public static void QuickSort<T>(T[] a)
where T : IComparable<T>
{
QuickSort(a, 0, a.Length - 1);
}
/// <summary>
/// クイックソート → 挿入ソートに切り替える配列長の閾値。
/// </summary>
const int THREASHOLD = 64;
/// <summary>
/// 挿入ソート。
/// 配列のどこからどこまでをソートするかを指定するバージョン。
/// </summary>
/// <param name="a">対象の配列</param>
/// <param name="first">ソート対象の先頭インデックス</param>
/// <param name="last">ソート対象の末尾インデックス</param>
static void InsertSort<T>(T[] a, int first, int last)
where T : IComparable<T>
{
for (int i = first + 1; i <= last; i++)
for (int j = i; j > first && a[j - 1].CompareTo(a[j]) > 0; --j)
Swap(ref a[j], ref a[j - 1]);
}
/// <summary>
/// クイックソート本体。
/// 配列のどこからどこまでをソートするかを指定するバージョン。
/// </summary>
/// <param name="a">対象の配列</param>
/// <param name="first">ソート対象の先頭インデックス</param>
/// <param name="last">ソート対象の末尾インデックス</param>
static void QuickSort<T>(T[] a, int first, int last)
where T : IComparable<T>
{
// 要素数が少なくなってきたら挿入ソートに切り替え
if (last - first < THREASHOLD)
{
InsertSort(a, first, last);
return;
}
// 枢軸決定(配列の先頭、ど真ん中、末尾の3つの値の中央値を使用。)
T pivot = Median(a[first], a[(first + last) / 2], a[last]);
// 左右分割
int l = first;
int r = last;
while(l <= r)
{
while (l < last && a[l].CompareTo(pivot) < 0) l++;
while (r > first && a[r].CompareTo(pivot) >= 0) r--;
if (l > r) break;
Swap(ref a[l], ref a[r]);
l++; r--;
}
// 再帰呼び出し
QuickSort(a, first, l-1);
QuickSort(a, l, last);
}
/// <summary>
/// 3つの値の中央値を求める。
/// </summary>
/// <param name="a">オペランドa</param>
/// <param name="b">オペランドb</param>
/// <param name="c">オペランドc</param>
/// <returns>中央値</returns>
static T Median<T>(T a, T b, T c)
where T : IComparable<T>
{
if (a.CompareTo(b) > 0) Swap(ref a, ref b);
if (a.CompareTo(c) > 0) Swap(ref a, ref c);
if (b.CompareTo(c) > 0) Swap(ref b, ref c);
return b;
}