Javaでのクイックソートアルゴリズムの実装

Javaでのクイックソートアルゴリズムの実装

1. 概要

このチュートリアルでは、Java実装に焦点を当てて、QuickSortアルゴリズムを詳細に検討します。

また、その長所と短所について説明し、時間の複雑さを分析します。

2. クイックソートアルゴリズム

Quicksort is a sorting algorithm, which is leveraging the divide-and-conquer principle. 平均的なO(n log n)の複雑さを持ち、特にビッグデータ量に対して最も使用されるソートアルゴリズムの1つです。

It’s important to remember that Quicksort isn’t a stable algorithm. 安定ソートアルゴリズムは、同じ値を持つ要素が、入力リストに表示されるのと同じ順序でソートされた出力に表示されるアルゴリズムです。

入力リストは、ピボットと呼ばれる要素によって2つのサブリストに分割されます。ピボットより小さい要素を持つ1つのサブリストと、ピボットより大きい要素を持つ別のサブリスト。 このプロセスは、サブリストごとに繰り返されます。

最後に、ソートされたすべてのサブリストがマージされて、最終出力が形成されます。

2.1. アルゴリズムの手順

  1. リストからピボットと呼ばれる要素を選択します。 これを使用して、リストを2つのサブリストに分割します。

  2. ピボットの周りのすべての要素の順序を変更します。値の小さいものがその前に配置され、ピボットの後のすべての要素がその後に配置されます。 このステップの後、ピボットは最終位置にあります。 これは重要なパーティション手順です。

  3. ピボットの左右の両方のサブリストに上記の手順を再帰的に適用します。

ご覧のとおり、quicksort is naturally a recursive algorithm, like every divide and conquer approach.

このアルゴリズムをよりよく理解するために、簡単な例を見てみましょう。

Arr[] = {5, 9, 4, 6, 5, 3}
  1. 簡単にするために、ピボットとして5を選択するとします。

  2. まず、5未満のすべての要素を配列の最初の位置に配置します:\ {3、4、5、6、5、9}

  3. 次に、3をピボットとして、左側のサブ配列\ {3,4}に対してこれを繰り返します。

  4. 3未満の要素はありません

  5. ピボットの右側のサブ配列にクイックソートを適用します。 {4}

  6. このサブ配列は、ソートされた1つの要素のみで構成されます

  7. 元の配列の右部分である\ {6、5、9}を使用して、最終的な順序付き配列を取得します

2.2. 最適なピボットの選択

QuickSortの重要なポイントは、最適なピボットを選択することです。 当然ながら、リストを2つの等しいサブリストに分割するため、中央の要素が最適です。

しかし、順序付けられていないリストから中央の要素を見つけることは難しく、時間がかかります。そのため、最初の要素、最後の要素、中央値またはその他のランダム要素をピボットとして使用します。

3. Javaでの実装

最初のメソッドはquickSort()で、パラメーターとして、ソートされる配列、最初と最後のインデックスを取ります。 まず、インデックスをチェックし、ソートする要素がまだある場合にのみ続行します。

ソートされたピボットのインデックスを取得し、それを使用して、quickSort()メソッドと同じパラメーターで、ただしインデックスが異なるpartition()メソッドを再帰的に呼び出します。

public void quickSort(int arr[], int begin, int end) {
    if (begin < end) {
        int partitionIndex = partition(arr, begin, end);

        quickSort(arr, begin, partitionIndex-1);
        quickSort(arr, partitionIndex+1, end);
    }
}

partition()メソッドを続けましょう。 簡単にするために、この関数は最後の要素をピボットとして使用します。 次に、各要素をチェックし、値が小さい場合はピボットの前に交換します。

パーティションの終わりまでに、ピボットよりも小さいすべての要素がその左側にあり、ピボットよりも大きいすべての要素がその右側にあります。 ピボットは最終的なソート位置にあり、関数はこの位置を返します。

private int partition(int arr[], int begin, int end) {
    int pivot = arr[end];
    int i = (begin-1);

    for (int j = begin; j < end; j++) {
        if (arr[j] <= pivot) {
            i++;

            int swapTemp = arr[i];
            arr[i] = arr[j];
            arr[j] = swapTemp;
        }
    }

    int swapTemp = arr[i+1];
    arr[i+1] = arr[end];
    arr[end] = swapTemp;

    return i+1;
}

4. アルゴリズム分析

4.1. 時間の複雑さ

最良の場合、アルゴリズムはリストを2つの等しいサイズのサブリストに分割します。 したがって、完全なnサイズのリストの最初の反復にはO(n)が必要です。 残りの2つのサブリストをn/2要素でソートするには、それぞれ2*O(n/2)かかります。 その結果、QuickSortアルゴリズムの複雑さはO(n log n)になります。

最悪の場合、アルゴリズムは各反復で1つの要素のみを選択するため、O(n) + O(n-1) + … + O(1)O(n2)と等しくなります。

平均して、QuickSortはO(n log n)の複雑さを持っているため、ビッグデータボリュームに適しています。

4.2. クイックソートとマージソート

マージソートではなくクイックソートを選択する必要がある場合について説明しましょう。

クイックソートとマージソートの平均時間計算量はO(n log n)ですが、クイックソートはO(log(n))の空間計算量を持っているため、推奨されるアルゴリズムです。 一方、マージソートはO(n)の追加ストレージを必要とするため、配列にはかなりのコストがかかります。

クイックソートは、操作のために異なるインデックスにアクセスする必要がありますが、連続したブロックがないため、リンクリストではこのアクセスを直接行うことはできません。したがって、要素にアクセスするには、リンクリストの先頭から各ノードを反復処理する必要があります。 また、マージソートはLinkedLists.用の余分なスペースなしで実装されます

このような場合、QuicksortおよびMergesortのオーバーヘッドの増加が一般的に好まれます。

5. 結論

Quicksortは、ほとんどの場合に非常に役立つエレガントなソートアルゴリズムです。

これは通常、「インプレース」アルゴリズムであり、平均時間計算量はO(n log n).です。

言及すべきもう1つの興味深い点は、JavaのArrays.sort()メソッドがプリミティブの配列をソートするためにクイックソートを使用していることです。 実装は2つのピボットを使用し、単純なソリューションよりもはるかに優れたパフォーマンスを発揮します。そのため、本番コードの場合、通常はライブラリメソッドを使用する方が適切です。

いつものように、このアルゴリズムを実装するためのコードは、on our GitHub repositoryにあります。