Как оптимизировать quicksort

Я пытаюсь разработать эффективный quicksort algo. Он работает нормально, но занимает много времени, когда количество элементов огромно, а некоторые разделы массива предварительно отсортированы. Я искал статью в Википедии на quicksort и там я нашла это написано:

чтобы убедиться, что используется не более o(log N), сначала рекурсируйте в меньшую половину массива и используйте хвостовой вызов для рекурсии в другую.

используйте сортировку вставки, которая имеет меньший постоянный коэффициент и, следовательно, быстрее на малых массивах, для вызовов на таких малых массивах (т. е. где длина меньше порога t, определенного экспериментально). Это можно реализовать, оставив такие массивы несортированными и выполнив один проход сортировки вставки в конце, потому что сортировка вставки обрабатывает почти отсортированные массивы эффективно. Отдельный вид вставки каждого небольшого сегмента по мере их идентификации добавляет накладные расходы на запуск и остановку многих небольших видов, но позволяет избежать тратить усилия на сравнение ключей по многим границам сегмента, какие ключи будут в порядке из-за работы процесса quicksort. Это также улучшает использование кэша.

Я сейчас рекурсией для обоих разделов. Есть идеи, как реализовать первый совет? Что значит рекурсия в меньшую половину массива, и использовать хвост вызова рекурсии в другой? А во-вторых, как я могу реализовать insertion-sort в пределах быстрой сортировки? Будет ли это всегда? повысить эффективность или только при предварительной сортировке определенных разделов массива? Если это второй случай, то, конечно, у меня нет способа узнать, когда это произойдет. Итак, когда я должен включить insertion-sort?

6 ответов


в Quick-sort вы выбираете случайный поворот, который ограничивает массив двумя половинами, большинство шансов, что один может быть меньше,

например, размер массива 100, pivot ограничивает массив 40 / 60, 40-это меньший размер.

предположим, что вы решили использовать пороговый размер для сортировки вставки 10, вам нужно продолжать рекурсивно разбивать массив на pivot, когда одна из половинок становится меньше или равна 10, вы можете использовать сортировку вставки это ведет себя как O(n) на массивах малого размера.

учтите, что сортировка вставки будет вести себя плохо, если Ваш массив отсортирован обратно (в худшем случае).

что касается материала рекурсии, вам нужно только изменить стоп-кейс рекурсии быстрой сортировки -> размер массива

по хвостовой рекурсии они означают сделать все , что вам нужно с помощью первая половина, а затем вызвать сортировку вставки для меньшей половины в качестве последнего метода, используется для экономии места.

  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

насколько я вижу, вторая оптимизация предлагает не использовать сортировку вставки для каждого шага рекурсии, но помните индексы, для которых сделано ограничение, а затем вызвать сортировку вставки в одном пакете, объединяющем элементы из всех срезов, это обеспечит улучшение использования кэша, но немного сложнее реализовать,


существует несколько способов сделать стандартную quicksort более эффективной. Чтобы реализовать первый совет из вашего поста вы должны написать что-то типа:

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. Это скажет вашему алгоритму, когда он должен переключиться на сортировку вставки. Должно быть довольно легко (вопрос добавления одного if-оператора) изменить предыдущий пример, чтобы он переключился на сортировку вставки после того, как он достиг точки среза, поэтому я оставлю вас на это.

Что касается метода раздела, я бы рекомендовал использовать раздел Lomuto вместо Hoare.

однако, если ваши данные уже предварительно отсортированы, то вы могли бы рассмотреть возможность использования другого алгоритма в целом. По моему опыту,сортировка слияния естественных рядов реализовано в связанном списке-очень хороший выбор, если ваши данные предварительно отсортированы.


некоторое время назад я написал алгоритм на основе quicksort, который вы можете найти там (на самом деле это алгоритм выбора, но также может использоваться алгоритм сортировки):

из этого опыта я извлек следующие уроки:

  1. тщательно настройте цикл разделов вашего алгоритма. Это часто недооценивается, но вы получите значительное повышение производительности, если позаботитесь о написании циклов, которые компилятор/процессор сможет программировать конвейер. Только это привело к победе около 50% в CPU cyles.
  2. ручное кодирование мелких сортов дает вам крупный выигрыш peformance. Когда количество элементов для сортировки в разделе составляет менее 8 элементов, просто не пытайтесь рекурсировать, а вместо этого реализуйте жестко закодированную сортировку, используя только ifs и swaps (посмотрите функция fast_small_sort в этом коде). Это может привести к выигрышу около 50% в циклах CPU, давая quicksort такую же практическую производительность, как хорошо написанная "сортировка слияния".
  3. потратьте время, чтобы выбрать лучшее значение поворота, когда обнаружен "плохой" выбор поворота. Моя реализация начинает использовать алгоритм "медиана медианы" для выбора поворота всякий раз, когда выбор поворота приводит к тому, что одна сторона находится под 16% оставшихся элементов для сортировки. это стратегия смягчения для наихудшей производительности quick-sort, и помогите убедиться, что на практике верхняя граница также O(n*log(n)) вместо O (n^2).
  4. оптимизация для массивов с большим количеством равных значений (при необходимости). Если сортируемые массивы имеют много равных значений, стоит оптимизировать, так как это приведет к плохому выбору pivot. В моем коде я делаю это, подсчитывая все записи массива, которые равны сводному значению. Это позволяет мне лечить pivot и все равные значения в массиве быстрее и не ухудшают производительность, когда она неприменима. это еще одна стратегия смягчения для наихудшей производительности, она помогает уменьшить использование стека наихудшего случая, резко снижая максимальный уровень рекурсии.

надеюсь, это поможет, Лоран.


вы можете взглянуть на TimSort, который для не полностью случайных данных работает лучше, чем quicksort (они имеют ту же асимптотическую сложность, но TimSort имеет более низкие константы)


недавно я нашел оптимизация. Он работает быстрее, чем std::sort. Он использует сортировку выбора на небольших массивах и median-of-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);
}

хвостовая рекурсия-это изменение рекурсивного вызова в цикл. Для 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                                                                         

кроме того, нет причин вызывать InsertionSort на небольших интервалах, потому что, когда QuickSort завершен, массив грубо отсортирован, так что осталось только небольшие интервалы для сортировки. И это как раз идеальный случай для 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