C++でクイックソートを実装するプログラム|ランダム化で最悪ケースO(n²)を回避
クイックソート(Quick Sort)は「分割統治法(divide-and-conquer)」に基づく高速な整列アルゴリズムです。平均時間計算量は O(n log n) と非常に効率的ですが、ピボットの選び方次第では最悪ケースで O(n²) まで計算量が悪化する可能性があります。
そこで本記事では、乱数を用いてピボットをランダムに選択する「ランダム化クイックソート」をC++で実装し、最悪ケースが発生する確率を大幅に下げる方法を解説します。
アルゴリズム
Partition(int a[], int l, int h)
配列 a の範囲 [l, h] を、ピボットより小さいグループと大きいグループに分割する関数です。末尾の要素をピボットとして扱い、ピボット未満の要素を左側へ寄せた後、ピボットを正しい位置(index)へ移動します。戻り値はピボットの最終的なインデックスです。
Begin
pivot = h // 末尾の要素をピボットとする
index = l
start = l、end = h
while start < end do
while a[start] <= a[pivot] AND start < end do
start = start + 1
done
while a[end] > a[pivot] do
end = end − 1
done
if start < end then
a[start] と a[end] を入れ替える
done
a[l] = a[end]
a[end] = a[pivot]
return end
End
RandomPivotPartition(int a[], int l, int h)
rand() を使って範囲 [l, h] からランダムにピボットを選び、それを末尾(h)の要素と入れ替えてから通常の Partition を呼び出す関数です。入力が既に整列済みなど偏っている場合でも、最悪ケース O(n²) に陥る可能性を効果的に低減できます。
Begin
n = rand()
pivot = l + n % (h − l + 1)
a[h] と a[pivot] を入れ替える
return Partition(a, l, h)
End
QuickSort(int a[], int l, int h)
整列処理の本体となる再帰関数です。ピボットの位置 pindex を基準に、左部分列 [l, pindex−1] と右部分列 [pindex+1, h] に対してそれぞれ QuickSort を再帰的に呼び出すことで、全体を整列させます。
Begin
int pindex
if (l < h)
pindex = RandomPivotPartition(a, l, h)
QuickSort(a, l, pindex − 1)
QuickSort(a, pindex + 1, h)
return 0
End
C++による実装例
以下は、上記アルゴリズムをそのままC++で実装した完全なサンプルコードです。ユーザーから要素数と各要素の値を入力として受け取り、整列後の結果を出力します。
#include<iostream>
#include<cstdlib>
using namespace std;
void swap(int *a, int *b) {
int temp;
temp = *a;
*a = *b;
*b = temp;
}
int Partition(int a[], int l, int h) {
int pivot, index, i;
index = l;
pivot = h;
for(i = l; i < h; i++) {
if(a[i] < a[pivot]) {
swap(&a[i], &a[index]);
index++;
}
}
swap(&a[pivot], &a[index]);
return index;
}
int RandomPivotPartition(int a[], int l, int h) {
int pvt, n, temp;
n = rand();
pvt = l + n%(h-l+1);
swap(&a[h], &a[pvt]);
return Partition(a, l, h);
}
int QuickSort(int a[], int l, int h) {
int pindex;
if(l < h) {
pindex = RandomPivotPartition(a, l, h);
QuickSort(a, l, pindex-1);
QuickSort(a, pindex+1, h);
}
return 0;
}
int main() {
int n, i;
cout<<"\nEnter the number of data element to be sorted: ";
cin>>n;
int arr[n];
for(i = 0; i < n; i++) {
cout<<"Enter element "<<i+1<<": ";
cin>>arr[i];
}
QuickSort(arr, 0, n-1);
cout<<"\nSorted Data ";
for (i = 0; i < n; i++)
cout<<"->"<<arr[i];
return 0;
}
実行結果
Enter the number of data element to be sorted: 4 Enter element 1: 3 Enter element 2: 4 Enter element 3: 7 Enter element 4: 6 Sorted Data ->3->4->6->7
計算量のまとめ
- 平均時間計算量:O(n log n)
- 最悪時間計算量:O(n²)(ランダムピボット選択により発生確率は極めて低い)
- 空間計算量:O(log n)(再帰呼び出しのスタック領域分)
このように、単純なクイックソートにランダム化を加えるだけで、特定の入力パターンによる性能劣化を防ぎ、実用上ほぼ常に O(n log n) に近い性能を安定して得られるようになります。
-
C++でバケットソートを実装する方法【アルゴリズムとサンプルコードを解説】
バケットソートとはバケットソート(Bucket Sort)は、データ要素を複数の「バケット(桶)」に分配してから整列を行うソート手法です。各バケットには性質の似たデータが格納され、分配後は各バケット内を別のソートアルゴリズム(ここでは標準ライブラリの sort)で整列します。最後にすべてのバケットの要素を元の配列へ順番に集めることで、全体がソートされた状態になります。このアルゴリズムは、入力データが0.0以上1.0未満のような一様な分布に従う場合に特に高い性能を発揮します。バケットソートの計算量時間計算量: 最良ケース・平均ケースで O(n + k)、最悪ケースで O(n²)空間計算量: 最悪
-
C++で基数ソート(ラディックスソート)を実装するプログラム
基数ソート(ラディックスソート)は、非比較型のソートアルゴリズムの一つです。要素同士を直接比較するのではなく、整数キーを構成する各桁に注目し、同じ桁位置・同じ値を持つ数字どうしをグループ化しながら並べ替えを行います。 「基数」とは記数法における底のことです。私たちが普段使う10進法では基数は10であるため、10進数を基数ソートで並べ替える際には、数値を一時的に格納するための10個のバケット(ポケット)が必要になります。 基数ソートの計算量 時間計算量: O(nk) ※nは要素数、kは最大桁数 空間計算量: O(n+k) 入力 − ソート前のデータ: 802 630 20 745 52 3