クイックソートを実装するとき、やらなければならないことの1つは、ピボットを選択することです。しかし、次のような擬似コードを見ると、ピボットを選択する方法が明確ではありません。リストの最初の要素?他に何か?
function quicksort(array)
var list less, greater
if length(array) ≤ 1
return array
select and remove a pivot value pivot from array
for each x in array
if x ≤ pivot then append x to less
else append x to greater
return concatenate(quicksort(less), pivot, quicksort(greater))
誰かが私がピボットを選択する概念と、異なるシナリオが異なる戦略を必要とするかどうかを理解するのを助けることができますか?.
ランダムピボットを選択すると、最悪の場合のO(n2)パフォーマンス(常に最初または最後を選択すると、ほぼソートされたデータまたはほぼ逆にソートされたデータに対して最悪のパフォーマンスが発生します)。ほとんどの場合、中間要素を選択することもできます。
また、これを自分で実装している場合、インプレースで機能するアルゴリズムのバージョンがあります(つまり、2つの新しいリストを作成してから連結することはありません)。
要件によって異なります。ピボットをランダムに選択すると、O(N ^ 2)パフォーマンスを生成するデータセットを作成するのが難しくなります。 「中央値3」(最初、最後、中間)も問題を回避する方法です。ただし、比較の相対的なパフォーマンスに注意してください。比較にコストがかかる場合、Mo3はランダムに(単一のピボット値)を選択するよりも多くの比較を行います。データベースレコードは、比較するとコストがかかる場合があります。
更新:コメントを回答に追加。
mdkess アサート:
「中央値3」は最初の最後の中間ではありません。 3つのランダムインデックスを選択し、この中間値を取ります。全体のポイントは、ピボットの選択が決定論的でないことを確認することです-そうである場合、最悪の場合のデータは非常に簡単に生成できます。
私が応答したもの:
つの中央分離帯を使用したHoareの検索アルゴリズムの分析 (1997)P Kirschenhofer、H Prodinger、CMartínezによる競合をサポートします(「3つの中央値」は3つのランダムなアイテムです)。
portal.acm.org に記述された記事があります。これは、The Computer Journal、Vol 27、No 3、1984に掲載されたHannuErkiöによる「メディアンの3つのクイックソートの最悪ケース順列」に関するものです。 。[2012-02-26更新: 記事 のテキストを取得しました。セクション2「アルゴリズム」の始まり:「A [L:R]の最初、中間、および最後の要素の中央値を使用することにより、最も実用的な状況。 'このように、最初から最後までのMo3アプローチについて議論しています。]
もう1つの興味深い興味深い記事は、M。D. McIlroyによるものです "Quicksortのキラーの敵" 、Software-Practice and Experience、Vol。 29(0)、1–4(0 1999)。ほとんどすべてのQuicksortを二次的に動作させる方法を説明します。
AT&T Bell Labs Tech Journal、1984年10月「ワーキングソートルーチンの構築における理論と実践」には、「Hoareはいくつかのランダムに選択された行の中央値を分割することを提案しました。Sedgewick[...]は最初の[。 ..]最後の[...]および中間」。これは、「3の中央値」の両方の手法が文献で知られていることを示しています。 (2014-11-23更新:この記事は、 IEEE Xplore または Wiley から入手できるようです。メンバーシップがある場合、または料金を支払う準備ができている場合。)
'Engineering a Sort Function' JL BentleyおよびMD McIlroy著、Software Practice and Experience、Vol 23(11)、1993年11月に公開され、問題の広範な議論に入り、彼らは適応型を選択しましたデータセットのサイズに一部基づいた分割アルゴリズム。さまざまなアプローチのトレードオフについて多くの議論があります。
「3の中央値」をGoogleで検索すると、さらに追跡できます。
情報のおかげで;以前、決定論的な「3の中央値」にしか遭遇していませんでした。
へえ、私はちょうどこのクラスを教えました。
いくつかのオプションがあります。
シンプル:範囲の最初または最後の要素を選択します。 (部分的にソートされた入力では悪い)より良い:範囲の中央のアイテムを選択します。 (部分的にソートされた入力の方が良い)
ただし、任意の要素を選択すると、サイズnの配列がサイズ1とn-1の2つの配列にうまく分割されないというリスクが生じます。それを十分に頻繁に行うと、クイックソートはO(n ^ 2)になるリスクがあります。
私が見た改善の1つは、中央値(最初、最後、中)を選択することです。最悪の場合でも、O(n ^ 2)に到達する可能性がありますが、確率的に、これはまれなケースです。
ほとんどのデータでは、最初または最後を選択するだけで十分です。ただし、最悪のシナリオ(部分的に並べ替えられた入力)に頻繁に遭遇する場合、最初のオプションは中心値を選択することです(これは、部分的に並べ替えられたデータの統計的に適切なピボットです)。
それでも問題が発生する場合は、中央値ルートに進みます。
固定のピボットを選択することは決してありません-これは攻撃される可能性があり、アルゴリズムの最悪の場合のO(n ^ 2)ランタイムを悪用します。 Quicksortのワーストケースランタイムは、パーティショニングの結果、1つの要素の1つの配列とn-1個の要素の1つの配列が生じるときに発生します。最初の要素をパーティションとして選択するとします。誰かがアルゴリズムを降順で配列にフィードすると、最初のピボットが最大になるため、配列内の他のすべてがその左側に移動します。その後、再帰すると、最初の要素が再び最大になるので、もう一度すべてを左に配置する、というようになります。
より良い方法は、中央値3の方法です。この方法では、3つの要素をランダムに選択し、中央を選択します。選択した要素は最初または最後ではないことを知っていますが、中心極限定理により、中間要素の分布は正規になるため、中間に向かう傾向があります(したがって、 、n lg n時間)。
アルゴリズムのO(nlgn)ランタイムを絶対に保証したい場合、配列の中央値を見つける5列の方法はO(n)時間。これは、最悪の場合のクイックソートの繰り返し方程式がT(n)= =O(n)(中央値を見つける)+ O(n)(partition)+ 2T(n/2)(左と右に再帰)。マスター定理により、これはO(n lg n)です。ただし、定数係数は非常に大きくなり、最悪の場合のパフォーマンスが主な関心事である場合は、代わりにマージソートを使用します。これは、平均してクイックソートよりも少し遅く、O(nlgn) time(このラメ中央値クイックソートよりもはるかに高速になります)。
あまりにも賢くなりすぎて、ピボット戦略を組み合わせようとしないでください。最初、最後、および中央のランダムインデックスの中央値を選択することにより、3の中央値とランダムピボットを組み合わせた場合、3次の中央値を送信する多くの分布に対して脆弱です(したがって、実際にはプレーンランダムピボット)
たとえば、パイプオルガン分布(1,2,3 ... N/2..3,2,1)の最初と最後は両方とも1で、ランダムインデックスは1より大きい数になり、中央値は1(最初または最後のいずれか)と、非常に不均衡なパーティション分割を取得します。
クイックソートをこれを行う3つのセクションに分けるのは簡単です
これは、1つの長い関数よりもわずかに非効率的ですが、理解しやすいものです。
コードは次のとおりです。
/* This selects what the data type in the array to be sorted is */
#define DATATYPE long
/* This is the swap function .. your job is to swap data in x & y .. how depends on
data type .. the example works for normal numerical data types .. like long I chose
above */
void swap (DATATYPE *x, DATATYPE *y){
DATATYPE Temp;
Temp = *x; // Hold current x value
*x = *y; // Transfer y to x
*y = Temp; // Set y to the held old x value
};
/* This is the partition code */
int partition (DATATYPE list[], int l, int h){
int i;
int p; // pivot element index
int firsthigh; // divider position for pivot element
// Random pivot example shown for median p = (l+h)/2 would be used
p = l + (short)(Rand() % (int)(h - l + 1)); // Random partition point
swap(&list[p], &list[h]); // Swap the values
firsthigh = l; // Hold first high value
for (i = l; i < h; i++)
if(list[i] < list[h]) { // Value at i is less than h
swap(&list[i], &list[firsthigh]); // So swap the value
firsthigh++; // Incement first high
}
swap(&list[h], &list[firsthigh]); // Swap h and first high values
return(firsthigh); // Return first high
};
/* Finally the body sort */
void quicksort(DATATYPE list[], int l, int h){
int p; // index of partition
if ((h - l) > 0) {
p = partition(list, l, h); // Partition list
quicksort(list, l, p - 1); // Sort lower partion
quicksort(list, p + 1, h); // Sort upper partition
};
};
ランダムにアクセス可能なコレクション(配列など)を並べ替える場合は、一般的に物理的な中間アイテムを選択するのが最善です。これにより、配列がすべてソート済み(またはほぼソート済み)の場合、2つのパーティションはほぼ均等になり、最高の速度が得られます。
線形アクセスのみ(リンクリストなど)で並べ替える場合は、最初のアイテムを選択するのが最善です。これは、アクセスが最も速いアイテムだからです。ただし、ここでリストが既に並べ替えられていると、ねじが締められます。一方のパーティションは常にnullになり、もう一方のパーティションにはすべてがあり、最悪の時間を生成します。
ただし、リンクリストの場合、最初のリスト以外のものを選択すると、事態がさらに悪化します。リストされたリストの中央のアイテムを選択し、各パーティションステップでそれをステップスルーする必要があります-O(N/2) logN回行われる操作を追加して合計しますtime O(1.5 N * log N)そして、それはリストが開始する前にどれくらいの長さかを知っている場合です-通常はそうではないので、それらをカウントするために最後までステップを踏む必要があります真ん中を見つけるために、実際のパーティションを実行するために3回目を実行します:O(2.5N * log N)
そもそもデータのソート方法に完全に依存しています。擬似ランダムと思われる場合、最善の策はランダムな選択を選択するか、中間を選択することです。
簡単に計算できるため、中央のインデックスを使用することをお勧めします。
丸めて計算できます(array.length/2)。
平均して、3の中央値は小さいnに適しています。中央値5は、nが大きいほど少し良くなります。 「3の中央値の中央値」である9番目の値は、非常に大きなnに対してさらに優れています。
サンプリングを高くするほど、nが大きくなるほど良くなりますが、サンプルを増やすと改善は劇的に遅くなります。また、サンプルのサンプリングとソートのオーバーヘッドが発生します。
クイックソートの複雑さは、ピボット値の選択によって大きく異なります。たとえば、常に最初の要素をピボットとして選択した場合、アルゴリズムの複雑さはO(n ^ 2)と同じくらい最悪になります。これは、ピボット要素を選択するスマートな方法です。1.配列の最初、中間、最後の要素を選択します。 2.これらの3つの数値を比較し、1より大きく、他の数値よりも小さい数値、つまり中央値を見つけます。 3.この要素をピボット要素にします。
この方法でピボットを選択すると、配列がほぼ半分に分割されるため、複雑さはO(nlog(n))に減少します。
理想的には、ピボットは配列全体の中央の値である必要があります。これにより、最悪の場合のパフォーマンスが得られる可能性が低くなります。