クイックソート:ピボットの選択


109

Quicksortを実装する場合、ピボットを選択する必要があります。しかし、以下のような疑似コードを見ると、ピボットをどのように選択すればよいかわかりません。リストの最初の要素?他に何か?

 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))

ピボットを選択する概念と、異なるシナリオが異なる戦略を必要とするかどうかを誰かが私に理解するのを助けることができますか?


回答:


87

ランダムピボットを選択すると、最悪の場合のO(n 2)パフォーマンスが発生する可能性が最小限になります(常に最初または最後を選択すると、並べ替えがほぼ完了したデータまたはほぼ逆に並べ替えられたデータのパフォーマンスが最悪になります)。中央の要素を選択することも、ほとんどの場合に許容されます。

また、これを自分で実装する場合、インプレースで機能するアルゴリズムのバージョンがあります(つまり、2つの新しいリストを作成してからそれらを連結することなく)。


10
自分で検索を実装するのは努力に値しないかもしれないという考えを私は2番目に考えます。また、乱数ジェネレーターはときどき遅いので、どのように乱数を選択するかに注意してください。
PeterAllenWebb 2008年

@ジョナサン・レフラーの答えはより良い
Nathan

60

要件によって異なります。ピボットをランダムに選択すると、O(N ^ 2)パフォーマンスを生成するデータセットを作成することが難しくなります。「3つの中央値」(最初、最後、中間)も問題を回避する方法です。ただし、比較の相対的なパフォーマンスには注意してください。比較にコストがかかる場合、Mo3はランダムに(単一のピボット値)を選択するよりも多くの比較を行います。データベースのレコードは比較にコストがかかる可能性があります。


更新:コメントを回答に取り込みます。

mdkessがアサートされました:

「3の中央値」は最初と最後の中間ではありません。3つのランダムなインデックスを選択し、その中央の値を取ります。重要なのは、ピボットの選択が確定的でないことを確認することです。そうであれば、最悪の場合のデータが非常に簡単に生成される可能性があります。

私はそれに答えました:

  • Hoare's Find Algorithm With Median-of-Three Partition(1997)の分析(Pキルシェンホーファー、Hプロディンガー、Cマルティネス)はあなたのコンテンションをサポートしています(「3つのメディアン」は3つのランダムなアイテムです)。

  • The Computer Journal、Vol 27、No 3、1984. [Update 2012-02-に掲載されたHannuErkiöによる「中央値から3つのクイックソートの最悪のケースの順列」についての記事がportal.acm.orgにあります。 26:記事のテキストを入手した。セクション2 'アルゴリズム'の開始: ' A [L:R]の最初、中間、最後の要素の中央値を使用することにより、ほとんどの実際的な状況で、かなり等しいサイズの部分への効率的な分割を実現できます。'したがって、最初から最後のMo3アプローチについて説明しています。]

  • 興味深いもう1つの短い記事は、Software-Practice and Experience、Vol。2に掲載されたMD McIlroyによる「A Quicker Adversary for Quicksort」です。29(0)、1–4(0 ​​1999)。ほとんどすべてのQuicksortを2次関数で動作させる方法を説明しています。

  • AT&T Bell Labs Tech Journal、1984年10月「ワーキングソートルーチンの構築における理論と実践」には、「Hoareはいくつかのランダムに選択されたラインの中央値を分割することを提案しました。Sedgewick[...]は最初の中央値を選択することを推奨しました。 ..]最後の[...]と中間 "。これは、「中央値3」の両方の手法が文献で知られていることを示しています。(2014-11-23の更新:この記事は、IEEE XploreまたはWileyから入手できるようです—メンバーシップを持っているか、料金を支払う準備ができている場合。)

  • ソフトウェアの実践と経験、Vol 23(11)、1993年11月に発行されたJLベントレーとMD McIlroyによる「ソート機能のエンジニアリング」は、問題の広範囲にわたる議論に入り、一部に基づいて適応パーティションアルゴリズムを選択しました。データセットのサイズ。さまざまなアプローチのトレードオフについて多くの議論があります。

  • 「3つの中央値」のGoogle検索は、さらに追跡するのに適しています。

情報のおかげで; 私は以前、決定論的な「中央値」に遭遇しただけでした。


4
中央値3は、最初と最後の中間ではありません。3つのランダムなインデックスを選択し、その中央の値を取ります。重要なのは、ピボットの選択が確定的でないことを確認することです。そうであれば、最悪の場合のデータが非常に簡単に生成される可能性があります。
2009

私は、クイックソートとヒープソートの両方の優れた機能を組み合わせたabtイントロソートを読んでいました。中央値3を使用してピボットを選択するアプローチは、常に好ましいとは限りません。
Sumit Kumar Saha

4
ランダムなインデックスを選択する際の問題は、乱数ジェネレーターがかなり高価になることです。並べ替えのビッグOコストは増加しませんが、最初、最後、および中央の要素を選択した場合よりも遅くなる可能性があります。(現実の世界では、誰もあなたのクイックソートを遅くするような不自然な状況を作っているに違いありません。)
Kevin Chen

20

えっと、私はこのクラスを教えました。

いくつかのオプションがあります。
単純:範囲の最初または最後の要素を選択します。(部分的にソートされた入力では悪い)より良い:範囲の中央にあるアイテムを選択します。(部分的にソートされた入力の方が良い)

ただし、任意の要素を選択すると、サイズnの配列をサイズ1とn-1の2つの配列に分割するリスクが低くなります。それを頻繁に行うと、クイックソートはO(n ^ 2)になるリスクがあります。

私が見た1つの改善点は、中央値(最初、最後、中間)を選択することです。最悪の場合でもO(n ^ 2)に移動できますが、確率的にはこれはまれなケースです。

ほとんどのデータでは、最初または最後を選択するだけで十分です。ただし、最悪のシナリオに頻繁に遭遇している場合(部分的にソートされた入力)、最初のオプションは中央値を選択することです(これは部分的にソートされたデータの統計的に優れたピボットです)。

それでも問題が解決しない場合は、中央値のルートに進んでください。


1
クラスで実験を行い、ソートされた順序で配列からk個の最小要素を取得しました。次に、ランダム配列を生成し、最小ヒープ、またはランダム化された選択と固定ピボットクイックソートを使用して、比較の数をカウントしました。この「ランダムな」データでは、2番目のソリューションは最初のソリューションよりも平均でパフォーマンスが低下しました。ランダム化されたピボットに切り替えると、パフォーマンスの問題が解決します。したがって、おそらくランダムなデータであっても、固定ピボットはランダム化されたピボットよりもパフォーマンスが大幅に低下します。
ロバートS.バーンズ2013年

サイズnの配列をサイズ1とn-1の2つの配列に分割すると、なぜO(n ^ 2)になるリスクがあるのですか?
アーロンフランケ

サイズNの配列を想定します。サイズ[1、N-1]に分割します。次のステップは、右半分を[1、N-2]に分割することです。以下同様に、サイズ1のN個のパーティションを作成します。ただし、半分にパーティション分割する場合は、各ステップでN / 2の2つのパーティションを作成し、複雑さのLog(n)項を導きます。
Chris Cudmore

11

固定ピボットを選択することは決してありません-これは攻撃されて、アルゴリズムの最悪の場合のO(n ^ 2)ランタイムを悪用する可能性があります。パーティション化の結果、1要素の1つの配列とn-1要素の1つの配列が生成されると、Quicksortの最悪のケースのランタイムが発生します。最初の要素をパーティションとして選択するとします。誰かが配列を降順でアルゴリズムにフィードする場合、最初のピボットが最大になるため、配列内の他のすべてのピボットはその左側に移動します。次に、再帰すると、最初の要素が再び最大になるため、もう一度すべてをその左側に配置します。

より良い手法は、3つの要素をランダムに選択し、中央を選択する3の中央値法です。選択した要素が最初または最後ではないことはわかっていますが、中央極限定理によって、中央の要素の分布は正常になります。つまり、中央に向かう傾向があります(したがって、 、n lg n時間)。

アルゴリズムのO(nlgn)ランタイムを絶対に保証したい場合、配列の中央値を見つけるためのcolumns-of-5メソッドはO(n)時間で実行されます。つまり、最悪の場合のクイックソートの反復方程式はbe T(n)= O(n)(中央値を見つける)+ O(n)(パーティション)+ 2T(n / 2)(左右に再帰します。)マスター定理により、これはO(n lg n)です。 。ただし、定数係数は非常に大きくなるため、最悪の場合のパフォーマンスが主な懸念事項である場合は、代わりにマージソートを使用します。マージソートは、クイックソートよりも平均的に少し遅く、O(nlgn)時間を保証します(さらに高速です)この不完全な中央値クイックソートよりも)。

中央値アルゴリズムの中央値アルゴリズムの説明


6

賢くなりすぎて、ピボット戦略を組み合わせようとしないでください。最初、最後、中央のランダムインデックスの中央値を選択して、3の中央値とランダムピボットを組み合わせた場合でも、3次の中央値を送信する多くの分布に対して脆弱です(そのため、実際にはプレーンランダムピボット)

たとえば、パイプオルガンの分布(1,2,3 ... N / 2..3,2,1)の最初と最後は両方とも1であり、ランダムインデックスは1より大きい数になり、中央値は1(最初または最後のいずれか)で、非常に不均衡なパーティション分割が発生します。


2

これを行うには、クイックソートを3つのセクションに分割する方が簡単です。

  1. データ要素機能の交換または交換
  2. パーティション機能
  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
  };
};

1

そもそも、データのソート方法に完全に依存しています。疑似ランダムになると思われる場合は、ランダムな選択を選択するか、中央を選択するのが最善の策です。


1

ランダムアクセス可能なコレクション(配列など)を並べ替える場合は、物理的な中央のアイテムを選択するのが一般的です。これにより、配列がすべてソート済み(またはほぼソート済み)の場合、2つのパーティションはほぼ均等になり、最高の速度が得られます。

線形アクセスのみ(リンクリストなど)で何かを並べ替える場合は、最初のアイテムを選択するのが最善です。これは、アクセスするのが最も速いアイテムだからです。ただし、ここでリストがすでにソートされている場合、あなたはうんざりしています-1つのパーティションは常にnullであり、もう1つのパーティションはすべてを持ち、最悪の時間を生み出します。

ただし、リンクリストの場合、最初のリスト以外のものを選択すると、問題がさらに悪化します。リストされたリストの真ん中の項目を選択し、パーティションごとにステップを実行する必要があります-logN回実行されるO(N / 2)操作を追加して、合計時間O(1.5 N * log N)を作成しますリストが開始するまでの時間を知っている場合-通常はそうではないので、カウントするために最初から最後まで進み、途中で途中まで進んでから、次のステップに進む必要があります。実際のパーティションを実行する3回目:O(2.5N * log N)


0

理想的には、ピボットは配列全体の中央値である必要があります。これにより、最悪の場合のパフォーマンスが発生する可能性が低くなります。


1
ここで馬の前のカート。
ncmathsadist

0

クイックソートの複雑さは、ピボット値の選択によって大きく異なります。たとえば、常に最初の要素をピボットとして選択すると、アルゴリズムの複雑さはO(n ^ 2)と同じくらい最悪になります。これはピボット要素を選択するためのスマートな方法です-1.配列の最初、中間、最後の要素を選択します。2.これらの3つの数値を比較して、1より大きく、他の中央値より小さい数値を見つけます。3.この要素をピボット要素として作成します。

この方法でピボットを選択すると、配列がほぼ半分に分割されるため、複雑さがO(nlog(n))に減少します。


0

平均して、3の中央値は小さいnに適しています。中央値が5の場合、nが大きいほど少し良くなります。「3つの中央値の3つの中央値」である9番目は、非常に大きなnの場合にさらに優れています。

nが大きくなるほど、サンプリングの回数が多くなるほど、より良い結果が得られますが、サンプルを増やすと、改善は劇的に遅くなります。また、サンプルのサンプリングと並べ替えのオーバーヘッドが発生します。


0

簡単に計算できるため、ミドルインデックスの使用をお勧めします。

丸めによって計算できます(array.length / 2)。


-1

真に最適化された実装では、ピボットを選択する方法は配列のサイズに依存する必要があります。大規模な配列の場合、適切なピボットの選択により多くの時間を費やすことは報われます。完全な分析を行わなければ、「O(log(n))要素の真ん中」が良い出発だと思います。これには、追加のメモリを必要としないという追加のボーナスがあります。より大きなパーティションでテールコールを使用し、パーティショニングを配置する場合、アルゴリズムのほとんどすべての段階で同じO(log(n))追加メモリを使用します。


1
3つの要素の中間を見つけることは一定の時間で行うことができます。これ以上、基本的にサブ配列をソートする必要があります。nが大きくなると、もう一度並べ替えの問題に戻ります。
Chris Cudmore
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.