C++
 Computer >> コンピューター >  >> プログラミング >> C++

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) に近い性能を安定して得られるようになります。

  1. C++でバケットソートを実装する方法【アルゴリズムとサンプルコードを解説】

    バケットソートとはバケットソート(Bucket Sort)は、データ要素を複数の「バケット(桶)」に分配してから整列を行うソート手法です。各バケットには性質の似たデータが格納され、分配後は各バケット内を別のソートアルゴリズム(ここでは標準ライブラリの sort)で整列します。最後にすべてのバケットの要素を元の配列へ順番に集めることで、全体がソートされた状態になります。このアルゴリズムは、入力データが0.0以上1.0未満のような一様な分布に従う場合に特に高い性能を発揮します。バケットソートの計算量時間計算量: 最良ケース・平均ケースで O(n + k)、最悪ケースで O(n²)空間計算量: 最悪

  2. C++で基数ソート(ラディックスソート)を実装するプログラム

    基数ソート(ラディックスソート)は、非比較型のソートアルゴリズムの一つです。要素同士を直接比較するのではなく、整数キーを構成する各桁に注目し、同じ桁位置・同じ値を持つ数字どうしをグループ化しながら並べ替えを行います。 「基数」とは記数法における底のことです。私たちが普段使う10進法では基数は10であるため、10進数を基数ソートで並べ替える際には、数値を一時的に格納するための10個のバケット(ポケット)が必要になります。 基数ソートの計算量 時間計算量: O(nk) ※nは要素数、kは最大桁数 空間計算量: O(n+k) 入力 − ソート前のデータ: 802 630 20 745 52 3