23

私は効率的なquicksortアルゴを見つけようとしています。正常に動作しますが、要素の数が膨大で、配列の特定のセクションが事前に並べ替えられている場合、実行に時間がかかります。でウィキペディアの記事を調べていたところ、次のquicksortように書かれていることがわかりました。

最大でO(log N)スペースが使用されていることを確認するには、最初に配列の小さい方の半分に再帰し、末尾呼び出しを使用してもう一方に再帰します。

このような小さな配列での呼び出しには、定数係数が小さいため小さな配列で高速な挿入ソートを使用します(つまり、長さが実験的に決定されたしきい値t未満の場合)。これは、そのような配列をソートせずに残し、最後に単一の挿入ソートパスを実行することで実装できます。これは、挿入ソートがほぼソートされた配列を効率的に処理するためです。識別された各小さなセグメントの個別の挿入ソートは、多くの小さなソートを開始および停止するオーバーヘッドを追加しますが、クイックソートプロセスの動作により、多くのセグメント境界を越えてキーを比較する無駄な労力を回避します。また、キャッシュの使用も改善されます。

私は現在、両方のパーティションを繰り返しています。最初のヒントを実装する方法はありますか?最初に配列の小さい方の半分に再帰し、末尾呼び出しを使用して他の半分に再帰するとはどういう意味ですか?insertion-sortそして第二に、クイックソート内でどのように実装できますか?それは常に効率を改善しますか、それともアレイの特定のセクションが事前にソートされている場合にのみ改善されますか?2番目のケースの場合、もちろん、それがいつ発生するかを知る方法はありません。では、いつ含める必要がありinsertion-sortますか?

4

6 に答える 6

13

クイックソートでは、配列を2つの半分に区切るランダムなピボットを選択します。ほとんどの場合、1つは小さい可能性があります。

たとえば、配列サイズ100、ピボットは配列を40/60に区切り、40は小さいサイズです。

挿入ソートを使用するしきい値サイズを10に決定したとすると、半分の1つが10以下になるたびに、ピボットによって配列を再帰的に分割し続ける必要があります。次のように動作する挿入ソートを使用できます。小さいサイズの配列ではO(n)。

配列が逆にソートされている場合(最悪の場合)、挿入ソートの動作が悪くなることを考慮に入れてください。

再帰に関するものに関しては、クイックソート再帰の停止ケースを変更するだけで済みます->配列サイズ<= 10再帰を停止し、挿入ソートを使用してすべての配列(この再帰ステップでははるかに小さい)をソートします。

末尾再帰とは、前半で必要なすべてを実行し、最後のメソッドとして小さい方の半分の挿入ソートを呼び出すことを意味します。これは、スペースを節約するために使用されます。

  Quick-sort()
      choose a pivot
      move the smaller elements from left
      move the bigger elements from right
      quick-sort on the bigger half of the array

      if half is less then X
         only then do an insertion sort on the other half <- this is a tail recursion insertion sort 
      else
         quick sort on this half also

私が見る限り、2番目の最適化では、すべての再帰ステップに挿入ソートを使用しないことを提案していますが、制約が作成されたインデックスを覚えてから、すべてのスライスからのアイテムを連結する1つのバッチで挿入ソートを呼び出すと、これにより保証されますキャッシュの使用を改善しますが、実装が少し難しくなります。

于 2012-09-17T07:48:39.340 に答える
8

標準のクイックソートをより効率的にする方法は複数あります。投稿の最初のヒントを実装するには、次のように書く必要があります。

void quicksort(int * tab, int l, int r)
{
   int q;
   while(l < r)
   {
      q = partition(tab, l, r);
      if(q - l < r - q) //recurse into the smaller half
      {
         quicksort(tab, l, q - 1);
         l = q + 1;
      } else
      {
         quicksort(tab, q + 1, r);
         r = q - 1;
      }
   }
}

それが十分に明確であることを願っています。次のステップは、再帰呼び出しを使用する代わりに、独自のスタックを実装することです(または、使用している言語の組み込みを使用します)。(擬似)コードの例:

void quicksort2(int * tab, int l, int r)
{
    int le, ri, q;
    init stack;
    push(l, r, stack);
    while(!empty(stack))
    {
        //take the top pair of values from the stack and set them to le and ri
        pop(le, ri, stack);
        if(le >= ri)
            continue;
        q = partition(tab, le, ri);
        if(q - le < ri - q) //smaller half goes first
        {
            push(le, q - 1, stack);
            push(q + 1, ri, stack);
        } else
        {
            push(q + 1, ri, stack);
            push(le, q - 1, stack);
        }
    }
    delete stack;
}

次に、投稿から他のヒントを実装するために進むことができます。これを行うには、任意の定数を設定する必要があります。これをCUT_OFFと呼び、約20に設定します。これにより、アルゴリズムが挿入ソートにいつ切り替わるかがわかります。前の例を変更して、CUT_OFFポイントに達した後に挿入ソートに切り替わるようにするのは、かなり簡単なはずです(ifステートメントを1つ追加するだけです)。

パーティションの方法としては、Hoareの代わりにLomutoパーティションを使用することをお勧めします。

ただし、データがすでに事前に並べ替えられている場合は、まったく別のアルゴリズムを使用することを検討できます。私の経験から、データが事前に並べ替えられている場合は、リンクリストに実装された自然系列のマージソートが非常に良い選択です。

于 2012-09-17T08:32:34.300 に答える
5

私は少し前に、そこで見つけることができるクイックソートベースのアルゴリズムを書きました(実際にはそれは選択アルゴリズムですが、ソートアルゴリズムも使用できます):

この経験から学んだ教訓は次のとおりです。

  1. アルゴリズムのパーティションループを注意深く調整します。これはしばしば過小評価されますが、コンパイラ/ CPUがソフトウェアパイプラインに接続できるループの記述に注意を払えば、パフォーマンスが大幅に向上します。これだけでも、CPUサイクルで約50%の勝利につながりました。
  2. 小さな種類を手作業でコーディングすると、パフォーマンスが大幅に向上します。パーティションでソートされる要素の数が8要素未満の場合は、わざわざ再帰を試みないでください。代わりに、ifとスワップのみを使用してハードコードされたソートを実装してください(このコードのfast_small_sort関数を参照してください)。 。これにより、CPUサイクルで約50%の勝利が得られ、クイックソートは適切に記述された「マージソート」と同じ実用的なパフォーマンスが得られます。
  3. 「不十分な」ピボット選択が検出された場合は、時間をかけてより適切なピボット値を選択してください。私の実装では、ピボット選択によって片側がソートされる残りの要素の16%未満になるたびに、ピボット選択に「中央値の中央値」アルゴリズムの使用を開始します。これは、クイックソートの最悪の場合のパフォーマンスを軽減する戦略であり、実際には上限もO(n ^ 2)ではなくO(n * log(n))になるようにするのに役立ちます。
  4. 等しい値がたくさんある配列用に最適化します(必要な場合)。並べ替える配列に等しい値がたくさんある場合は、ピボットの選択が不十分になるため、最適化する価値があります。私のコードでは、ピボット値に等しいすべての配列エントリをカウントすることによってそれを行います。これにより、ピボットと配列内のすべての等しい値をより高速に処理でき、適用できない場合でもパフォーマンスが低下することはありません。これは、最悪の場合のパフォーマンスに対するもう1つの緩和戦略であり、最大再帰レベルを大幅に減らすことで、最悪の場合のスタック使用量を減らすのに役立ちます

これがお役に立てば幸いです、ローラン。

于 2012-09-17T11:03:56.050 に答える
1

完全にランダムではないデータの場合、クイックソートよりもパフォーマンスが優れているTimSortを見ることができます(それらは同じ漸近的な複雑さを持っていますが、TimSortの定数は低くなっています)

于 2012-09-17T08:52:20.087 に答える
1

私は最近、この最適化を見つけました。std::sortよりも高速に動作します。小さな配列の選択ソートと3の中央値をパーティション要素として使用します。

これは私のC++実装です:

const int CUTOFF = 8;

template<typename T>
bool less (T &v, T &w)
{
    return (v < w);
}

template<typename T>
bool eq (T &v, T &w)
{
    return w == v;
}

template <typename T>
void swap (T *a, T *b)
{
    T t = *a;
    *a = *b;
    *b = t;
}

template<typename T>
void insertionSort (vector<T>& input, int lo, int hi) 
{
    for (int i = lo; i <= hi; ++i)
    {
        for (int j = i; j > lo && less(input[j], input[j-1]); --j)
        {
            swap(&input[j], &input[j-1]);
        }
    }
}


template<typename T>
int median3 (vector<T>& input, int indI, int indJ, int indK)
{
    return (less(input[indI], input[indJ]) ?
            (less(input[indJ], input[indK]) ? indJ : less(input[indI], input[indK]) ? indK : indI) :
            (less(input[indK], input[indJ]) ? indJ : less(input[indK], input[indI]) ? indK : indI));
}


template <typename T>
void sort(vector<T>& input, int lo, int hi) 
{ 
    int lenN = hi - lo + 1;

    // cutoff to insertion sort
    if (lenN <= CUTOFF) 
    {
        insertionSort(input, lo, hi);
        return;
    }

    // use median-of-3 as partitioning element
    else if (lenN <= 40) 
    {
        int median = median3(input, lo, lo + lenN / 2, hi);
        swap(&input[median], &input[lo]);
    }

    // use Tukey ninther as partitioning element
    else  
    {
        int eps = lenN / 8;
        int mid = lo + lenN / 2;
        int mFirst = median3(input, lo, lo + eps, lo + eps + eps);
        int mMid = median3(input, mid - eps, mid, mid + eps);
        int mLast = median3(input, hi - eps - eps, hi - eps, hi); 
        int ninther = median3(input, mFirst, mMid, mLast);
        swap(&input[ninther], &input[lo]);
    }

    // Bentley-McIlroy 3-way partitioning
    int iterI = lo, iterJ = hi + 1;
    int iterP = lo, iterQ = hi + 1;

    for (;; ) 
    {
        T v = input[lo];
        while (less(input[++iterI], v))
        {
            if (iterI == hi) 
                break;
        }
        while (less(v, input[--iterJ]))
        {
            if (iterJ == lo)    
                break;
        }
        if (iterI >= iterJ) 
            break;
        swap(&input[iterI], &input[iterJ]);
        if (eq(input[iterI], v)) 
            swap(&input[++iterP], &input[iterI]);
        if (eq(input[iterJ], v)) 
            swap(&input[--iterQ], &input[iterJ]);
    }
    swap(&input[lo], &input[iterJ]);

    iterI = iterJ + 1;
    iterJ = iterJ - 1;
    for (int k = lo + 1; k <= iterP; ++k) 
    {
        swap(&input[k], &input[iterJ--]);
    }
    for (int k = hi  ; k >= iterQ; --k)
    {
        swap(&input[k], &input[iterI++]);
    }

    sort(input, lo, iterJ);
    sort(input, iterI, hi);
}
于 2013-10-16T06:41:52.963 に答える
1

末尾再帰とは、再帰呼び出しをループに変更することです。QuickSortの場合、次のようになります。

QuickSort(SortVar)                                                                     
   Granularity = 10                                                            
   SortMax = Max(SortVar)
   /* Put an element after the last with a higher key than all other elements 
      to avoid that the inner loop goes on forever */
   SetMaxKey(SortVar, SortMax+1)

   /* Push the whole interval to sort on stack */               
   Push 1 SortMax                                                              
   while StackSize() > 0                                                       
      /* Pop an interval to sort from stack */
      Pop SortFrom SortTo                                                     

      /* Tail recursion loop */                           
      while SortTo - SortFrom >= Granularity                                

         /* Find the pivot element using median of 3 */                            
         Pivot = Median(SortVar, SortFrom, (SortFrom + SortTo) / 2, SortTo)             
         /* Put the pivot element in front */                                     
         if Pivot > SortFrom then Swap(SortVar, SortFrom, Pivot)

         /* Place elements <=Key to the left and elements >Key to the right */           
         Key = GetKey(SortVar, SortFrom)                                                
         i = SortFrom + 1                                                      
         j = SortTo                                                            
         while i < j                                                        
            while GetKey(SortVar, i) <= Key; i = i + 1; end                          
            while GetKey(SortVar, j) > Key; j = j - 1; end                           
            if i < j then Swap(SortVar, i, j)                                       
         end                                                                   

         /* Put the pivot element back */                            
         if GetKey(SortVar, j) < Key then Swap(SortVar, SortFrom, j)                                         

         if j - SortFrom < SortTo - j then                                  
            /* The left part is smallest - put it on stack */                     
            if j - SortFrom > Granularity then Push SortFrom j-1               
            /* and do tail recursion on the right part */                           
            SortFrom = j + 1                                                   
         end                                                                   
         else
            /* The right part is smallest - put it on stack */                       
            if SortTo - j > Granularity then Push j+1 SortTo                   
            /* and do tail recursion on the left part */                         
            SortTo = j - 1                                                     
         end                                                                   
      end                                                                      
   end                                                                         

   /* Run insertionsort on the whole array to sort the small intervals */    
   InsertionSort(SortVar)                                                          
return                                                                         

さらに、QuickSortが終了すると配列が大まかにソートされ、ソートする間隔が小さいため、小さい間隔でInsertionSortを呼び出す理由はありません。そして、これはInsertionSortの完璧なケースです。

スタックがない場合は、代わりに再帰を使用できますが、末尾再帰は保持します。

QuickSort(SortVar, SortFrom, SortTo)                                                                     
   Granularity = 10                                                            

   /* Tail recursion loop */                           
   while SortTo - SortFrom >= Granularity                                

      /* Find the pivot element using median of 3 */                            
      Pivot = Median(SortVar, SortFrom, (SortFrom + SortTo) / 2, SortTo)             
      /* Put the pivot element in front */                                     
      if Pivot > SortFrom then Swap(SortVar, SortFrom, Pivot)

      /* Place elements <=Key to the left and elements >Key to the right */           
      Key = GetKey(SortVar, SortFrom)                                                
      i = SortFrom + 1                                                      
      j = SortTo                                                            
      while i < j                                                        
         while GetKey(SortVar, i) <= Key; i = i + 1; end                          
         while GetKey(SortVar, j) > Key; j = j - 1; end                           
         if i < j then Swap(SortVar, i, j)                                       
      end                                                                   

      /* Put the pivot element back */                            
      if GetKey(j) < Key then Swap(SortVar, SortFrom, j)                                         

      if j - SortFrom < SortTo - j then                                  
         /* The left part is smallest - recursive call */                     
         if j - SortFrom > Granularity then QuickSort(SortVar, SortFrom, j-1)           
         /* and do tail recursion on the right part */                           
         SortFrom = j + 1                                                   
      end                                                                   
      else
         /* The right part is smallest - recursive call */                       
         if SortTo - j > Granularity then QuickSort(SortVar, j+1, SortTo)                   
         /* and do tail recursion on the left part */                         
         SortTo = j - 1                                                     
      end                                                                   
   end                                                                         

   /* Run insertionsort on the whole array to sort the small intervals */    
   InsertionSort(SortVar)                                                          
return                                                                         
于 2014-03-05T13:40:26.507 に答える