web-dev-qa-db-ja.com

QuickSortがn log nである理由の直観的な説明

QuickSort nをlog nにする理由について、直感的でありながら正式な「平易な英語」を説明できる人はいますか?私の理解では、n個のアイテムを渡す必要があり、このログをn回実行します...なぜこのログをn回実行するのかを言葉に入れる方法がわかりません。

46
Jim_CS

各パーティショニング操作はO(n)操作(配列で1パス)を取ります。平均して、各パーティショニングは配列を2つの部分に分割します(合計でn回の操作になります)。合計でO(n * log n)操作があります。

つまり平均してn回のパーティション操作で、各パーティションはO(n)操作を必要とします。

45
Eugene Retunsky

複雑

クイックソートは、入力を2つのチャンクに分割することから開始します。「ピボット」値を選択し、入力をピボット値より小さい値とピボット値より大きい値に分割します(もちろん、ピボット値に等しい値はもちろん、どちらかに行きますが、基本的な説明のために、それらが最終的にどのようになるかは重要ではありません)。

入力は(定義により)ソートされていないため、そのようにパーティション化するには、入力内のすべての項目を調べる必要があるため、O(N)操作です。最初の入力では、これらの「チャンク」のそれぞれを再帰的にソートします。これらの再帰呼び出しのそれぞれは、その入力のすべてを調べますので、2つの呼び出しの間、すべての入力値にアクセスします(再び)。パーティション化の「レベル」では、すべての入力項目を調べる1つの呼び出しがあります。2番目のレベルでは、2つのパーティション化ステップがありますが、2つの間で、すべての入力項目を(再び)見ていきます。ただし、合計で各レベルの呼び出しはすべての入力項目を調べます。

パーティションのサイズの下限に達するまで、入力をますます小さく分割し続けます。最小の可能性があるのは、各パーティションの単一のアイテムです。

理想的なケース

理想的なケースでは、各分割ステップで入力が半分に分割されることを望みます。 「半分」はおそらく正確には等しくありませんが、ピボットをうまく選択すれば、かなり近いはずです。計算を簡単に保つために、完全なパーティション分割を想定して、毎回正確な半分を取得します。

この場合、半分に分割できる回数は入力数の2を底とする対数になります。たとえば、128個の入力が与えられた場合、64、32、16、8、4、2、および1のパーティションサイズが得られます。2(128)= 7)。

したがって、log(N)パーティション「レベル」があり、各レベルはすべてのN個の入力にアクセスする必要があります。したがって、log(N)レベルにレベルあたりN操作を掛けると、O(N log N)の全体的な複雑さが得られます。

最悪の場合

ここで、各パーティションレベルが入力を正確に半分に「分割」するという仮定を再検討しましょう。分割要素の選択の程度によっては、正確に半分になるとは限りません。それで、起こりうる最悪のことは何ですか?最悪の場合は、実際には入力の最小または最大の要素であるピボットです。この場合、O(N)パーティショニングレベルを行いますが、サイズが等しい2つの半分を取得する代わりに、1つの要素の1つのパーティションとNの1つのパーティションになります-1要素:パーティション分割のすべてのレベルでそれが発生する場合、パーティションが1つの要素に達する前にO(N)パーティション分割レベルを実行することになります。

これにより、Quicksortの技術的に正しいbig-Oの複雑度が得られます(big-Oは公式には複雑度の上限を指します)。 O(N)パーティションレベルがあり、各レベルにO(N)ステップが必要であるため、O(N * N)(すなわち、O(N2)複雑さ。

実用的な実装

実際問題として、実際の実装は、通常、単一の要素のパーティションに実際に到達する前にパーティション化を停止します。典型的なケースでは、パーティションに含まれる要素が10個以下の場合、パーティション化を停止し、挿入ソートのようなものを使用します(通常、少数の要素の方が高速であるため)。

変更されたアルゴリズム

最近では、Quicksortに対する他の変更(Introsort、PDQ Sortなど)が発明されました。2) 最悪の場合。 Introsortは、現在のパーティショニングの「レベル」を追跡することでこれを行います。深すぎる場合は、通常の入力ではQuicksortよりも遅いヒープソートに切り替わりますが、O(N log N)の複雑さを保証します。任意の入力に対して。

PDQソートはそれにさらに別の工夫を加えます:ヒープソートは遅いため、可能な場合はヒープソートへの切り替えを回避しようとします。それには、ピボット値が不十分になっているように見える場合は、ピボット。その後、十分に優れたピボット値を生成できない場合(およびその場合のみ)、代わりにヒープソートの使用に切り替えます。

112
Jerry Coffin

まあ、それは常にn(log n)ではありません。選択したピボットがほぼ中央にあるときのパフォーマンス時間です。最悪の場合、最小または最大の要素をピボットとして選択すると、時間はO(n ^ 2)になります。

「n log n」を視覚化するために、ピボットが、ソートされる配列内のすべての要素の平均に最も近い要素であると想定できます。これにより、配列はほぼ同じ長さの2つの部分に分割されます。これらの両方で、クイックソート手順を適用します。

各ステップのように、配列の長さを半分にすると、length = 1に達するまで(つまり、1要素のソートされた配列)、log n(base 2)回これを行います。

2
Siddharth Gaur

対数の裏には重要な直観があります。

1に達する前に定数で数値nを除算できる回数はO(log n)です。

言い換えると、O(log n)という用語を持つランタイムが表示された場合、一定の係数で繰り返し縮小する何かを見つける可能性が高くなります。

クイックソートでは、一定の要因で縮小しているのは、各レベルでの最大の再帰呼び出しのサイズです。クイックソートは、ピボットを選択し、配列をピボットより小さい要素とピボットより大きい要素の2つのサブ配列に分割し、各サブ配列を再帰的にソートすることで機能します。

ピボットをランダムに選択した場合、選択されたピボットが要素の中央の50%にある可能性は50%です。つまり、2つのサブアレイの大きい方が最大で75%になる可能性が80%あります。オリジナルのサイズ。 (理由がわかりますか?)

したがって、クイックソートが時間O(n log n)で実行される理由の良い直観は次のとおりです。再帰ツリーの各レイヤーはO(n)動作し、各再帰呼び出しには配列のサイズを少なくとも25%縮小する可能性が高いため、要素を使い果たす前にO(log n)層が存在し、配列から破棄されることが予想されます。

もちろん、これは、ピボットをランダムに選択していることを前提としています。クイックソートの多くの実装では、ヒューリスティックを使用して、あまり多くの作業をせずにNiceピボットを取得しようとしますが、残念ながら、これらの実装は最悪の場合、全体的なランタイムの低下につながる可能性があります。この質問に対する@Jerry Coffinの優れた答えは、使用するソートアルゴリズムを切り替えることでO(n log n)の最悪の場合の動作を保証するクイックソートのいくつかのバリエーションについて語っています。これは、これに関する詳細を探すのに最適な場所です。

0
templatetypedef

ソートアルゴリズムを2つの部分に分けます。 1つ目は、パーティション分割と2つ目の再帰呼び出しです。分割の複雑さはO(N)であり、理想的なケースの再帰呼び出しの複雑さはO(logN)です。たとえば、4つの入力がある場合、2(log4)再帰呼び出し。両方を掛けるとO(NlogN)が得られます。これは非常に基本的な説明です。

0
IT Worker

実際には、すべてのN個の要素(ピボット)の位置を見つける必要がありますが、比較の最大数は各要素のlogNです(最初はN、2番目のピボットN/2、3番目のN/4です。中央値要素)

0
Prdp