125

クイックソートを実装するとき、やらなければならないことの 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))

ピボットを選択する概念と、異なるシナリオが異なる戦略を必要とするかどうかを理解するのを手伝ってくれる人はいますか?

4

13 に答える 13

99

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

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

于 2008-10-02T19:41:36.657 に答える
70

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


更新:コメントを回答に引き出します。

mdkessは主張しました:

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

私は次のように答えました。

  • Analysis Of Hoare's Find Algorithm With Median-Of-Three Partition (1997) by P Kirschenhofer, H Prodinger, C Martínez はあなたの主張を支持しています (「median-of-three」は 3 つのランダムな項目です)。

  • The Computer Journal、Vol 27、No 3、1984 に掲載された Hannu Erkiö による「The Worst Case Permutation for Median-of-Three Quicksort」に関するportal.acm.orgで説明されている記事があります。 [Update 2012-02- 26:記事のテキストを取得しました。セクション 2「アルゴリズム」は次のように始まります。 ' A[L:R] の最初、中間、および最後の要素の中央値を使用することにより、ほとんどの実際的な状況で、かなり等しいサイズの部分への効率的な分割を実現できます。したがって、最初-中間-最後の Mo3 アプローチについて議論しています。]

  • もう 1 つの興味深い短い記事は、MD McIlroy による「A Killer Adversary for Quicksort」で、Software-Practice and Experience, Vol. 29(0)、1-4 (0 1999)。ほぼすべての Quicksort を 2 次的に動作させる方法について説明します。

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

  • JL Bentley と MD McIlroy による「Engineering a Sort Function」 (Software Practice and Experience、Vol 23(11)、1993 年 11 月) では、問題の広範な議論が行われ、一部はデータセットのサイズ。さまざまなアプローチのトレードオフについて多くの議論があります。

  • 「median-of-three」を Google で検索すると、さらに追跡することができます。

情報のおかげで; 私は以前、決定論的な「3 の中央値」にしか遭遇したことがありませんでした。

于 2008-10-02T19:42:39.760 に答える
24

へー、私はちょうどこのクラスを教えました。

いくつかのオプションがあります。
シンプル: 範囲の最初または最後の要素を選択します。(部分的にソートされた入力ではダメ) (部分的にソートされた入力の方が優れています)

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

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

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

それでも問題が解決しない場合は、中間ルートに進みます。

于 2008-10-02T19:46:49.420 に答える
17

絶対に固定ピボットを選択しないでください。これは、アルゴリズムの最悪のケースの O(n 2 ) ランタイムを悪用するために攻撃される可能性があります。これは、問題を引き起こしているだけです。クイックソートの最悪のケースの実行時間は、パーティショニングの結果が 1 つの要素の 1 つの配列と n-1 要素の 1 つの配列になる場合に発生します。最初の要素をパーティションとして選択するとします。誰かが配列をアルゴリズムに降順でフィードした場合、最初のピボットが最大になるため、配列内の他のすべてがその左側に移動します。次に、再帰すると、最初の要素が再び最大になるため、もう一度すべてをその左側に配置します。

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

アルゴリズムの O(nlog(n)) 実行時間を絶対に保証したい場合、配列の中央値を見つけるための列数 5 の方法は O(n) 時間で実行されます。最悪の場合は次のようになります。

T(n) = O(n) (find the median) + O(n) (partition) + 2T(n/2) (recurse left and right)

マスター定理により、これは O(nlog(n)) です。ただし、一定の要因は非常に大きくなります。最悪の場合のパフォーマンスが主な関心事である場合は、代わりにマージソートを使用してください。これは、平均してクイックソートよりもわずかに遅く、O(nlog(n)) 時間を保証します (そしてこの不十分な中央値のクイックソートよりもはるかに高速です)。

Median of Medians アルゴリズムの説明

于 2008-10-25T21:50:38.060 に答える
7

巧妙になりすぎてピボット戦略を組み合わせようとしないでください。最初、最後、および中間のランダム インデックスの中央値を選択して 3 の中央値をランダム ピボットと組み合わせた場合でも、3 の 2 次の中央値を送る分布の多くに対して脆弱です (したがって、実際には、単純なランダム ピボット)

たとえば、パイプ オルガンの分布 (1,2,3...N/2..3,2,1) の最初と最後は両方とも 1 になり、ランダム インデックスは 1 より大きい数値になり、中央値を取ると 1 (最初か最後か)、非常にバランスの取れていないパーティショニングが得られます。

于 2008-10-26T03:54:41.723 に答える
3

これを行うと、クイックソートを 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
  };
};
于 2011-03-10T02:19:41.167 に答える
2

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

線形アクセスのみで何かを並べ替える場合 (リンクされたリストなど)、最初のアイテムを選択するのが最善です。最初のアイテムが最も速くアクセスできるからです。ただし、ここで、リストが既にソートされている場合は、失敗します。一方のパーティションは常に null になり、もう一方のパーティションにはすべてが含まれるため、最悪の事態が発生します。

ただし、リンクされたリストの場合、最初のもの以外を選択すると、事態はさらに悪化します。リストされたリストの中央の項目を選択します。各パーティションステップでそれをステップスルーする必要があります-logN回実行されるO(N / 2)操作を追加して、合計時間をO(1.5 N * log N)にしますそれは、開始する前にリストの長さがわかっている場合です。通常はそうではありません。そのため、最初から最後まで数えてから、途中まで行って真ん中を見つけ、次のステップを実行する必要があります。実際の分割を行う 3 回目: O(2.5N * log N)

于 2008-10-02T19:42:50.757 に答える
2

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

于 2008-10-02T19:46:15.477 に答える
0

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

サンプリングを高くするほど、n が増加するにつれて改善されますが、サンプル数を増やすと改善は劇的に遅くなります。また、サンプルのサンプリングと並べ替えのオーバーヘッドが発生します。

于 2016-10-19T10:04:39.287 に答える
0

簡単に計算できるので、中間指数を使用することをお勧めします。

(array.length / 2) を丸めることで計算できます。

于 2017-08-09T01:29:00.600 に答える