Quicksort: выбор стержня

При внедрении 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))

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

Ответ 1

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

Кроме того, если вы реализуете это самостоятельно, есть версии алгоритма, которые работают на месте (т.е. не создавая два новых списка, а затем объединяют их).

Ответ 2

Это зависит от ваших требований. Выбор стержня случайным образом затрудняет создание набора данных, который генерирует производительность O (N ^ 2). "Медиана трех" (первая, последняя, ​​средняя) также способ избежать проблем. Остерегайтесь относительной эффективности сравнений; если ваши сравнения являются дорогостоящими, тогда Mo3 делает больше сравнений, чем выбор (одно значение поворота) случайным образом. Записи базы данных могут стоить дорого.


Обновление: вытягивание комментариев в ответ.

mdkess утверждается:

"Медиана 3" НЕ первая последняя середина. Выберите три случайных индекса и возьмите среднее значение этого. Все дело в том, чтобы убедиться, что ваш выбор опорных точек не является детерминированным - если это так, данные о худшем случае могут быть легко сгенерированы.

На что я ответил:

  • Анализ алгоритма поиска хора с медианом трех разделов (1997) P Kirschenhofer, H Prodinger, C Martínez поддерживает ваше утверждение (что "медианная из трех" - это три случайных предмета).

  • В статье описана статья portal.acm.org, в которой говорилось о "наихудшей перестановке случаев для медиа-из трех Quicksort" Hannu Erkiö, опубликованный в The Computer Journal, Vol 27, No 3, 1984. [Обновление 2012-02-26: Получил текст статья. Раздел 2 "Алгоритм" начинается: "Используя медиану первого, среднего и последнего элементов A [L: R], эффективные разбиения на части достаточно равных размеров могут быть достигнуты в большинстве практических ситуаций". Таким образом, он обсуждает подход Mo3 с первым-средним последним.]

  • Еще одна короткая статья, которая интересна MD McIlroy, "Убийца для Quicksort" , опубликованный в Software-Practice и опыт, т. 29 (0), 1-4 (0 1999). В нем объясняется, как заставить почти любой Quicksort вести себя квадратично.

  • AT & T Bell Labs Tech Journal, октябрь 1984 г. "Теория и практика в построении рабочей упорядоченной рутины" гласит: "Хоар предложил разбиение вокруг медианы нескольких случайно выбранных строк. Sedgewick [...] рекомендуется выбирая медиану первого [...] последнего [...] и среднего". Это указывает на то, что в литературе известны обе методики "медианы трех". (Обновление 2014-11-23: статья, по-видимому, доступна в IEEE Xplore или из Wiley - если у вас есть членство или вы готовы заплатить сбор.)

  • "Инженерная функция сортировки" от JL Bentley и MD McIlroy, опубликованных в Software Practice and Experience, том 23 (11), Ноябрь 1993 года, подробно обсуждает проблемы, и они выбрали адаптивный алгоритм разделения, частично основанный на размере набора данных. Существует много обсуждений компромиссов для различных подходов.

  • Поиск Google для "медианных из трех" отлично подходит для дальнейшего отслеживания.

Спасибо за информацию; Раньше я встречался только с детерминированным "медианным из трех".

Ответ 3

Хех, я просто преподавал этот класс.

Существует несколько вариантов. Простой: выберите первый или последний элемент диапазона. (плохо на частично отсортированном входе) Лучше: выберите элемент в середине диапазона. (лучше на частично отсортированном входе)

Однако выбор любого произвольного элемента сопряжен с риском плохого разбиения массива размера n на два массива размером 1 и n-1. Если вы делаете это достаточно часто, ваш quicksort рискует стать O (n ^ 2).

Одним из улучшений, которые я видел, является медиана (первая, последняя, ​​средняя); В худшем случае он все равно может перейти к O (n ^ 2), но, вероятностно, это редкий случай.

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

Если у вас все еще возникают проблемы, перейдите к медианному маршруту.

Ответ 4

Никогда не выбирайте фиксированный стержень - это может быть атаковано, чтобы использовать ваш алгоритм наихудшего случая O (n ^ 2), который просто требует неприятностей. Quicksort худший случай времени выполнения возникает, когда разбиение на разделы приводит к одному массиву из 1 элемента и одному массиву из n-1 элементов. Предположим, вы выбрали первый элемент в качестве своего раздела. Если кто-то подает массив в ваш алгоритм, который находится в порядке убывания, ваш первый стержень будет самым большим, поэтому все остальное в массиве будет перемещаться влево от него. Затем, когда вы будете рекурсивно, первый элемент снова станет самым большим, поэтому еще раз вы положите все слева и т.д.

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

Если вы абсолютно хотите гарантировать время выполнения O (nlgn) для алгоритма, метод столбцов 5 для поиска медианы массива выполняется в O (n) времени, что означает, что уравнение повторения для quicksort в худшим случаем будет T (n) = O (n) (найдите медиану) + O (n) (разбиение) + 2T (n/2) (recurse слева и справа). По основной теореме это O (n lg n). Тем не менее, постоянный коэффициент будет огромным, и если производительность наихудшего случая является вашей основной задачей, вместо этого используйте сортировку слияния, которая в несколько раз медленнее, чем quicksort, и гарантирует время O (nlgn) (и будет намного быстрее чем эта хромая медианная быстрая сортировка).

Объяснение медианы медианного алгоритма

Ответ 5

Не пытайтесь усвоить и объединить стратегии поворота. Если вы комбинировали медиану 3 со случайным стержнем, выбирая медиану первого, последнего и случайного индекса в середине, тогда вы по-прежнему будете уязвимы ко многим распределениям, которые высылают медианную из 3 квадратичных (так что на самом деле это хуже, чем простой случайный стержень)

Например, распределение органов труб (1,2,3... N/2,3,2,1) в первом и последнем случае будет равным 1, а случайный индекс будет некоторое число больше 1, взяв медианное значение 1 ( либо первый, либо последний), и вы получаете экстренно неуравновешенное разделение.

Ответ 6

Если вы сортируете произвольно доступную коллекцию (например, массив), лучше всего выбрать физический средний элемент. При этом, если массив готов к сортировке (или почти отсортирован), эти два раздела будут близки к четным, и вы получите максимальную скорость.

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

Однако для связанного списка выбор чего-либо, кроме первого, только усугубит ситуацию. Он выбирает средний элемент в списке-списке, вам нужно будет пройти через него на каждом этапе раздела - добавив операцию O (N/2), которая будет выполняться с помощью logN раз, делая общее время O (1,5 N * log N) и если мы знаем, как долго список начинается до того, как мы начнем - обычно мы этого не делаем, мы должны пройти весь путь, чтобы посчитать их, затем шаг на полпути, чтобы найти середину, а затем пройти через в третий раз сделать фактический раздел: O (2.5N * log N)

Ответ 7

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

Ответ 8

Легче разбить quicksort на три секции, делая это

  • Функция элемента данных обмена или свопинга
  • Функция раздела
  • Обработка разделов

Это немного более неэффективно, чем одна длинная функция, но гораздо легче понять.

Код следует:

/* 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
  };
};

Ответ 9

В идеале ось должна быть средним значением во всем массиве. Это уменьшит вероятность получения наихудших результатов.

Ответ 10

Быстрая сортировка сортировки сильно зависит от выбора значения поворота. Например, если вы всегда можете выбрать первый элемент в качестве опоры, сложность алгоритма становится в худшем случае, как O (N ^ 2). вот умный метод выбора элемента-поворота - 1. выберите первый, средний, последний элемент массива. 2. сравните эти три числа и найдите число, которое больше единицы и меньше, чем другое, то есть медиана. 3. сделайте этот элемент как элемент поворота.

выбор стержня с помощью этого метода разбивает массив почти на две половины и, следовательно, сложность сводится к O (nlog (n)).

Ответ 11

В среднем Медиана 3 хороша для малых n. Медиана 5 немного лучше для больших n. Еще один, который является "медианным из трех медианов трех", еще лучше для очень больших n.

Чем выше вы используете выборку, тем лучше вы получаете с ростом n, но улучшение значительно замедляется по мере увеличения образцов. И вы несете накладные расходы на выборку и сортировку образцов.

Ответ 12

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

Вы можете вычислить его округлением (array.length/2).

Ответ 13

В действительно оптимизированной реализации метод выбора точки поворота должен зависеть от размера массива - для большого массива он рассчитывает потратить больше времени на выбор хорошего стержня. Не делая полного анализа, я бы предположил, что "средний элемент O (log (n)) - это хороший старт, и у этого есть дополнительный бонус, не требующий дополнительной памяти: использование tail-call на более крупном разделе и в то мы используем ту же O (log (n)) дополнительную память почти на каждом этапе алгоритма.